Skip to main content

hydro_lang/live_collections/
optional.rs

1//! Definitions for the [`Optional`] live collection.
2
3use std::cell::RefCell;
4use std::marker::PhantomData;
5use std::ops::Deref;
6use std::rc::Rc;
7
8use stageleft::{IntoQuotedMut, QuotedWithContext, q};
9use syn::parse_quote;
10
11use super::OperatorContext;
12use super::boundedness::{Bounded, Boundedness, IsBounded, Unbounded};
13use super::singleton::Singleton;
14use super::stream::{AtLeastOnce, ExactlyOnce, NoOrder, Stream, TotalOrder};
15use crate::compile::builder::{CycleId, FlowState};
16use crate::compile::ir::{CollectionKind, HydroIrOpMetadata, HydroNode, HydroRoot, SharedNode};
17#[cfg(stageleft_runtime)]
18use crate::forward_handle::{CycleCollection, CycleCollectionWithInitial, ReceiverComplete};
19use crate::forward_handle::{ForwardRef, TickCycle};
20use crate::live_collections::singleton::SingletonBound;
21#[cfg(feature = "tokio")]
22use crate::location::TopLevel;
23#[cfg(stageleft_runtime)]
24use crate::location::dynamic::{DynLocation, LocationId};
25use crate::location::tick::{Atomic, DeferTick};
26use crate::location::{Location, Tick, check_matching_location};
27use crate::nondet::{NonDet, nondet};
28use crate::prelude::KeyedSingleton;
29use crate::properties::{StreamMapFuncAlgebra, ValidMutCommutativityFor, ValidMutIdempotenceFor};
30
31/// A *nullable* Rust value that can asynchronously change over time.
32///
33/// Optionals are the live collection equivalent of [`Option`]. If the optional is [`Bounded`],
34/// the value is frozen and will not change. But if it is [`Unbounded`], the value will
35/// asynchronously change over time, including becoming present of uninhabited.
36///
37/// Optionals are used in many of the same places as [`Singleton`], but when the value may be
38/// nullable. For example, the first element of a [`Stream`] is exposed as an [`Optional`].
39///
40/// Type Parameters:
41/// - `Type`: the type of the value in this optional (when it is not null)
42/// - `Loc`: the [`Location`] where the optional is materialized
43/// - `Bound`: tracks whether the value is [`Bounded`] (fixed) or [`Unbounded`] (changing asynchronously)
44pub struct Optional<Type, Loc, Bound: Boundedness> {
45    pub(crate) location: Loc,
46    pub(crate) ir_node: Rc<RefCell<HydroNode>>,
47    pub(crate) flow_state: FlowState,
48
49    _phantom: PhantomData<(Type, Loc, Bound)>,
50}
51
52impl<T, L, B: Boundedness> Drop for Optional<T, L, B> {
53    fn drop(&mut self) {
54        let ir_node = self.ir_node.replace(HydroNode::Placeholder);
55        if !matches!(ir_node, HydroNode::Placeholder) && !ir_node.is_shared_with_others() {
56            self.flow_state.borrow_mut().try_push_root(HydroRoot::Null {
57                input: Box::new(ir_node),
58                op_metadata: HydroIrOpMetadata::new(),
59            });
60        }
61    }
62}
63
64impl<'a, T, L> From<Optional<T, L, Bounded>> for Optional<T, L, Unbounded>
65where
66    T: Clone,
67    L: Location<'a>,
68{
69    fn from(value: Optional<T, L, Bounded>) -> Self {
70        let tick = value.location().tick();
71        value.clone_into_tick(&tick).latest()
72    }
73}
74
75impl<'a, T, L> DeferTick for Optional<T, Tick<L>, Bounded>
76where
77    L: Location<'a>,
78{
79    fn defer_tick(self) -> Self {
80        Optional::defer_tick(self)
81    }
82}
83
84impl<'a, T, L> CycleCollection<'a, TickCycle> for Optional<T, Tick<L>, Bounded>
85where
86    L: Location<'a>,
87{
88    type Location = Tick<L>;
89
90    fn create_source(cycle_id: CycleId, location: Tick<L>) -> Self {
91        Optional::new(
92            location.clone(),
93            HydroNode::CycleSource {
94                cycle_id,
95                metadata: location.new_node_metadata(Self::collection_kind()),
96            },
97        )
98    }
99}
100
101impl<'a, T, L> CycleCollectionWithInitial<'a, TickCycle> for Optional<T, Tick<L>, Bounded>
102where
103    L: Location<'a>,
104{
105    type Location = Tick<L>;
106
107    fn location(&self) -> &Self::Location {
108        self.location()
109    }
110
111    fn create_source_with_initial(cycle_id: CycleId, initial: Self, location: Tick<L>) -> Self {
112        let from_previous_tick: Optional<T, Tick<L>, Bounded> = Optional::new(
113            location.clone(),
114            HydroNode::DeferTick {
115                input: Box::new(HydroNode::CycleSource {
116                    cycle_id,
117                    metadata: location.new_node_metadata(Self::collection_kind()),
118                }),
119                metadata: location
120                    .new_node_metadata(Optional::<T, Tick<L>, Bounded>::collection_kind()),
121            },
122        );
123
124        from_previous_tick.or(initial.filter_if(location.optional_first_tick(q!(())).is_some()))
125    }
126}
127
128impl<'a, T, L> ReceiverComplete<'a, TickCycle> for Optional<T, Tick<L>, Bounded>
129where
130    L: Location<'a>,
131{
132    fn complete(self, cycle_id: CycleId, expected_location: LocationId) {
133        assert_eq!(
134            Location::id(&self.location),
135            expected_location,
136            "locations do not match"
137        );
138        self.location
139            .flow_state()
140            .borrow_mut()
141            .push_root(HydroRoot::CycleSink {
142                cycle_id,
143                input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
144                op_metadata: HydroIrOpMetadata::new(),
145            });
146    }
147}
148
149impl<'a, T, L, B: Boundedness> CycleCollection<'a, ForwardRef> for Optional<T, L, B>
150where
151    L: Location<'a>,
152{
153    type Location = L;
154
155    fn create_source(cycle_id: CycleId, location: L) -> Self {
156        Optional::new(
157            location.clone(),
158            HydroNode::CycleSource {
159                cycle_id,
160                metadata: location.new_node_metadata(Self::collection_kind()),
161            },
162        )
163    }
164}
165
166impl<'a, T, L, B: Boundedness> ReceiverComplete<'a, ForwardRef> for Optional<T, L, B>
167where
168    L: Location<'a>,
169{
170    fn complete(self, cycle_id: CycleId, expected_location: LocationId) {
171        assert_eq!(
172            Location::id(&self.location),
173            expected_location,
174            "locations do not match"
175        );
176        self.location
177            .flow_state()
178            .borrow_mut()
179            .push_root(HydroRoot::CycleSink {
180                cycle_id,
181                input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
182                op_metadata: HydroIrOpMetadata::new(),
183            });
184    }
185}
186
187impl<'a, T, L, B: SingletonBound> From<Singleton<T, L, B>> for Optional<T, L, B::UnderlyingBound>
188where
189    L: Location<'a>,
190{
191    fn from(singleton: Singleton<T, L, B>) -> Self {
192        Optional::new(
193            singleton.location.clone(),
194            HydroNode::Cast {
195                inner: Box::new(singleton.ir_node.replace(HydroNode::Placeholder)),
196                metadata: singleton
197                    .location
198                    .new_node_metadata(Self::collection_kind()),
199            },
200        )
201    }
202}
203
204#[cfg(stageleft_runtime)]
205pub(super) fn zip_inside_tick<'a, T, O, L: Location<'a>, B: Boundedness>(
206    me: Optional<T, L, B>,
207    other: Optional<O, L, B>,
208) -> Optional<(T, O), L, B> {
209    check_matching_location(&me.location, &other.location);
210
211    Optional::new(
212        me.location.clone(),
213        HydroNode::CrossSingleton {
214            left: Box::new(me.ir_node.replace(HydroNode::Placeholder)),
215            right: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
216            metadata: me
217                .location
218                .new_node_metadata(Optional::<(T, O), L, B>::collection_kind()),
219        },
220    )
221}
222
223#[cfg(stageleft_runtime)]
224fn or_inside_tick<'a, T, L: Location<'a>, B: Boundedness>(
225    me: Optional<T, L, B>,
226    other: Optional<T, L, B>,
227) -> Optional<T, L, B> {
228    check_matching_location(&me.location, &other.location);
229
230    Optional::new(
231        me.location.clone(),
232        HydroNode::ChainFirst {
233            first: Box::new(me.ir_node.replace(HydroNode::Placeholder)),
234            second: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
235            metadata: me
236                .location
237                .new_node_metadata(Optional::<T, L, B>::collection_kind()),
238        },
239    )
240}
241
242impl<'a, T, L, B: Boundedness> Clone for Optional<T, L, B>
243where
244    T: Clone,
245    L: Location<'a>,
246{
247    fn clone(&self) -> Self {
248        if !matches!(self.ir_node.borrow().deref(), HydroNode::Tee { .. }) {
249            let orig_ir_node = self.ir_node.replace(HydroNode::Placeholder);
250            *self.ir_node.borrow_mut() = HydroNode::Tee {
251                inner: SharedNode(Rc::new(RefCell::new(orig_ir_node))),
252                metadata: self.location.new_node_metadata(Self::collection_kind()),
253            };
254        }
255
256        if let HydroNode::Tee { inner, metadata } = self.ir_node.borrow().deref() {
257            Optional {
258                location: self.location.clone(),
259                flow_state: self.flow_state.clone(),
260                ir_node: super::tracked_ir_node(
261                    &self.flow_state,
262                    HydroNode::Tee {
263                        inner: SharedNode(inner.0.clone()),
264                        metadata: metadata.clone(),
265                    },
266                ),
267                _phantom: PhantomData,
268            }
269        } else {
270            unreachable!()
271        }
272    }
273}
274
275impl<'a, T, L, B: Boundedness> Optional<T, L, B>
276where
277    L: Location<'a>,
278{
279    pub(crate) fn new(location: L, ir_node: HydroNode) -> Self {
280        debug_assert_eq!(ir_node.metadata().location_id, Location::id(&location));
281        debug_assert_eq!(ir_node.metadata().collection_kind, Self::collection_kind());
282        let flow_state = location.flow_state().clone();
283        let ir_node = super::tracked_ir_node(&flow_state, ir_node);
284        Optional {
285            location,
286            flow_state,
287            ir_node,
288            _phantom: PhantomData,
289        }
290    }
291
292    pub(crate) fn collection_kind() -> CollectionKind {
293        CollectionKind::Optional {
294            bound: B::BOUND_KIND,
295            element_type: stageleft::quote_type::<T>().into(),
296        }
297    }
298
299    /// Returns the [`Location`] where this optional is being materialized.
300    pub fn location(&self) -> &L {
301        &self.location
302    }
303
304    /// Creates a shared reference handle to this optional that can be captured inside `q!()`
305    /// closures. The handle resolves to `&Option<T>` at runtime.
306    ///
307    /// The optional must be bounded, otherwise reading it would be non-deterministic.
308    /// The handle can only be captured in closures passed to operators on collections at
309    /// the same location with **matching boundedness**; capturing it in a closure over an
310    /// unbounded collection is rejected at compile time.
311    pub fn by_ref(&self) -> crate::handoff_ref::OptionalRef<'a, '_, T, L, B>
312    where
313        B: IsBounded,
314    {
315        crate::handoff_ref::OptionalRef::new(&self.ir_node)
316    }
317
318    /// Returns a mutable reference handle to this optional that can be captured inside `q!()`
319    /// closures. The handle resolves to `&mut Option<T>` at runtime.
320    pub fn by_mut(&self) -> crate::handoff_ref::OptionalMut<'a, '_, T, L, B>
321    where
322        B: IsBounded,
323    {
324        crate::handoff_ref::OptionalMut::new(&self.ir_node)
325    }
326
327    /// Weakens the consistency of this live collection to not guarantee any consistency across
328    /// cluster members (if this collection is on a cluster).
329    pub fn weaken_consistency(self) -> Optional<T, L::DropConsistency, B>
330    where
331        L: Location<'a>,
332    {
333        if L::consistency()
334            .is_none_or(|c| c == crate::location::dynamic::ClusterConsistency::NoConsistency)
335        {
336            // already no consistency
337            Optional::new(
338                self.location.drop_consistency(),
339                self.ir_node.replace(HydroNode::Placeholder),
340            )
341        } else {
342            Optional::new(
343                self.location.drop_consistency(),
344                HydroNode::Cast {
345                    inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
346                    metadata: self
347                        .location
348                        .clone()
349                        .drop_consistency()
350                        .new_node_metadata(Optional::<T, L::DropConsistency, B>::collection_kind()),
351                },
352            )
353        }
354    }
355
356    /// Casts this live collection to have the consistency guarantees specified in the given
357    /// location type parameter. The developer must ensure that the strengthened consistency
358    /// is actually guaranteed, via the proof field (see [`crate::prelude::manual_proof`]).
359    pub fn assert_has_consistency_of<L2: Location<'a, DropConsistency = L::DropConsistency>>(
360        self,
361        _proof: impl crate::properties::ConsistencyProof,
362    ) -> Optional<T, L2, B>
363    where
364        L: Location<'a>,
365    {
366        if L::consistency() == L2::consistency() {
367            Optional::new(
368                self.location.with_consistency_of(),
369                self.ir_node.replace(HydroNode::Placeholder),
370            )
371        } else {
372            Optional::new(
373                self.location.with_consistency_of(),
374                HydroNode::AssertIsConsistent {
375                    inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
376                    trusted: false,
377                    metadata: self
378                        .location
379                        .clone()
380                        .with_consistency_of::<L2>()
381                        .new_node_metadata(Optional::<T, L2, B>::collection_kind()),
382                },
383            )
384        }
385    }
386
387    /// Transforms the optional value by applying a function `f` to it,
388    /// continuously as the input is updated.
389    ///
390    /// Whenever the optional is empty, the output optional is also empty.
391    ///
392    /// # Example
393    /// ```rust
394    /// # #[cfg(feature = "deploy")] {
395    /// # use hydro_lang::prelude::*;
396    /// # use futures::StreamExt;
397    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
398    /// let tick = process.tick();
399    /// let optional = tick.optional_first_tick(q!(1));
400    /// optional.map(q!(|v| v + 1)).all_ticks()
401    /// # }, |mut stream| async move {
402    /// // 2
403    /// # assert_eq!(stream.next().await.unwrap(), 2);
404    /// # }));
405    /// # }
406    /// ```
407    pub fn map<U, F>(self, f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>>) -> Optional<U, L, B>
408    where
409        F: Fn(T) -> U + 'a,
410    {
411        let f = f
412            .splice_fn1_ctx(&OperatorContext::<L, B>::new(&self.location))
413            .into();
414        Optional::new(
415            self.location.clone(),
416            HydroNode::Map {
417                f,
418                input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
419                metadata: self
420                    .location
421                    .new_node_metadata(Optional::<U, L, B>::collection_kind()),
422            },
423        )
424    }
425
426    /// Transforms the optional value by applying a function `f` to it and then flattening
427    /// the result into a stream, preserving the order of elements.
428    ///
429    /// If the optional is empty, the output stream is also empty. If the optional contains
430    /// a value, `f` is applied to produce an iterator, and all items from that iterator
431    /// are emitted in the output stream in deterministic order.
432    ///
433    /// The implementation of [`Iterator`] for the output type `I` must produce items in a
434    /// **deterministic** order. For example, `I` could be a `Vec`, but not a `HashSet`.
435    /// If the order is not deterministic, use [`Optional::flat_map_unordered`] instead.
436    ///
437    /// # Example
438    /// ```rust
439    /// # #[cfg(feature = "deploy")] {
440    /// # use hydro_lang::prelude::*;
441    /// # use futures::StreamExt;
442    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
443    /// let tick = process.tick();
444    /// let optional = tick.optional_first_tick(q!(vec![1, 2, 3]));
445    /// optional.flat_map_ordered(q!(|v| v)).all_ticks()
446    /// # }, |mut stream| async move {
447    /// // 1, 2, 3
448    /// # for w in vec![1, 2, 3] {
449    /// #     assert_eq!(stream.next().await.unwrap(), w);
450    /// # }
451    /// # }));
452    /// # }
453    /// ```
454    pub fn flat_map_ordered<U, I, F, C, Idemp, const WAS_MUT: bool>(
455        self,
456        f: impl IntoQuotedMut<'a, F, OperatorContext<L, Bounded>, StreamMapFuncAlgebra<C, Idemp>>,
457    ) -> Stream<U, L, Bounded, TotalOrder, ExactlyOnce>
458    where
459        B: IsBounded,
460        I: IntoIterator<Item = U>,
461        F: FnMut(T) -> I + 'a,
462        C: ValidMutCommutativityFor<F, T, I, TotalOrder, WAS_MUT>,
463        Idemp: ValidMutIdempotenceFor<F, T, I, ExactlyOnce, WAS_MUT>,
464    {
465        self.into_stream().flat_map_ordered(f)
466    }
467
468    /// Like [`Optional::flat_map_ordered`], but allows the implementation of [`Iterator`]
469    /// for the output type `I` to produce items in any order.
470    ///
471    /// If the optional is empty, the output stream is also empty. If the optional contains
472    /// a value, `f` is applied to produce an iterator, and all items from that iterator
473    /// are emitted in the output stream in non-deterministic order.
474    ///
475    /// # Example
476    /// ```rust
477    /// # #[cfg(feature = "deploy")] {
478    /// # use hydro_lang::{prelude::*, live_collections::stream::{NoOrder, ExactlyOnce}};
479    /// # use futures::StreamExt;
480    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test::<_, _, _, NoOrder, ExactlyOnce>(|process| {
481    /// let tick = process.tick();
482    /// let optional = tick.optional_first_tick(q!(
483    ///     std::collections::HashSet::<i32>::from_iter(vec![1, 2, 3])
484    /// ));
485    /// optional.flat_map_unordered(q!(|v| v)).all_ticks()
486    /// # }, |mut stream| async move {
487    /// // 1, 2, 3, but in no particular order
488    /// # let mut results = Vec::new();
489    /// # for _ in 0..3 {
490    /// #     results.push(stream.next().await.unwrap());
491    /// # }
492    /// # results.sort();
493    /// # assert_eq!(results, vec![1, 2, 3]);
494    /// # }));
495    /// # }
496    /// ```
497    pub fn flat_map_unordered<U, I, F, C, Idemp, const WAS_MUT: bool>(
498        self,
499        f: impl IntoQuotedMut<'a, F, OperatorContext<L, Bounded>, StreamMapFuncAlgebra<C, Idemp>>,
500    ) -> Stream<U, L, Bounded, NoOrder, ExactlyOnce>
501    where
502        B: IsBounded,
503        I: IntoIterator<Item = U>,
504        F: FnMut(T) -> I + 'a,
505        C: ValidMutCommutativityFor<F, T, I, TotalOrder, WAS_MUT>,
506        Idemp: ValidMutIdempotenceFor<F, T, I, ExactlyOnce, WAS_MUT>,
507    {
508        self.into_stream().flat_map_unordered(f)
509    }
510
511    /// Flattens the optional value into a stream, preserving the order of elements.
512    ///
513    /// If the optional is empty, the output stream is also empty. If the optional contains
514    /// a value that implements [`IntoIterator`], all items from that iterator are emitted
515    /// in the output stream in deterministic order.
516    ///
517    /// The implementation of [`Iterator`] for the element type `T` must produce items in a
518    /// **deterministic** order. For example, `T` could be a `Vec`, but not a `HashSet`.
519    /// If the order is not deterministic, use [`Optional::flatten_unordered`] instead.
520    ///
521    /// # Example
522    /// ```rust
523    /// # #[cfg(feature = "deploy")] {
524    /// # use hydro_lang::prelude::*;
525    /// # use futures::StreamExt;
526    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
527    /// let tick = process.tick();
528    /// let optional = tick.optional_first_tick(q!(vec![1, 2, 3]));
529    /// optional.flatten_ordered().all_ticks()
530    /// # }, |mut stream| async move {
531    /// // 1, 2, 3
532    /// # for w in vec![1, 2, 3] {
533    /// #     assert_eq!(stream.next().await.unwrap(), w);
534    /// # }
535    /// # }));
536    /// # }
537    /// ```
538    pub fn flatten_ordered<U>(self) -> Stream<U, L, Bounded, TotalOrder, ExactlyOnce>
539    where
540        B: IsBounded,
541        T: IntoIterator<Item = U>,
542    {
543        self.flat_map_ordered(q!(|v| v))
544    }
545
546    /// Like [`Optional::flatten_ordered`], but allows the implementation of [`Iterator`]
547    /// for the element type `T` to produce items in any order.
548    ///
549    /// If the optional is empty, the output stream is also empty. If the optional contains
550    /// a value that implements [`IntoIterator`], all items from that iterator are emitted
551    /// in the output stream in non-deterministic order.
552    ///
553    /// # Example
554    /// ```rust
555    /// # #[cfg(feature = "deploy")] {
556    /// # use hydro_lang::{prelude::*, live_collections::stream::{NoOrder, ExactlyOnce}};
557    /// # use futures::StreamExt;
558    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test::<_, _, _, NoOrder, ExactlyOnce>(|process| {
559    /// let tick = process.tick();
560    /// let optional = tick.optional_first_tick(q!(
561    ///     std::collections::HashSet::<i32>::from_iter(vec![1, 2, 3])
562    /// ));
563    /// optional.flatten_unordered().all_ticks()
564    /// # }, |mut stream| async move {
565    /// // 1, 2, 3, but in no particular order
566    /// # let mut results = Vec::new();
567    /// # for _ in 0..3 {
568    /// #     results.push(stream.next().await.unwrap());
569    /// # }
570    /// # results.sort();
571    /// # assert_eq!(results, vec![1, 2, 3]);
572    /// # }));
573    /// # }
574    /// ```
575    pub fn flatten_unordered<U>(self) -> Stream<U, L, Bounded, NoOrder, ExactlyOnce>
576    where
577        B: IsBounded,
578        T: IntoIterator<Item = U>,
579    {
580        self.flat_map_unordered(q!(|v| v))
581    }
582
583    /// Creates an optional containing only the value if it satisfies a predicate `f`.
584    ///
585    /// If the optional is empty, the output optional is also empty. If the optional contains
586    /// a value and the predicate returns `true`, the output optional contains the same value.
587    /// If the predicate returns `false`, the output optional is empty.
588    ///
589    /// The closure `f` receives a reference `&T` rather than an owned value `T` because filtering does
590    /// not modify or take ownership of the value. If you need to modify the value while filtering
591    /// use [`Optional::filter_map`] instead.
592    ///
593    /// # Example
594    /// ```rust
595    /// # #[cfg(feature = "deploy")] {
596    /// # use hydro_lang::prelude::*;
597    /// # use futures::StreamExt;
598    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
599    /// let tick = process.tick();
600    /// let optional = tick.optional_first_tick(q!(5));
601    /// optional.filter(q!(|&x| x > 3)).all_ticks()
602    /// # }, |mut stream| async move {
603    /// // 5
604    /// # assert_eq!(stream.next().await.unwrap(), 5);
605    /// # }));
606    /// # }
607    /// ```
608    pub fn filter<F>(self, f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>>) -> Optional<T, L, B>
609    where
610        F: Fn(&T) -> bool + 'a,
611    {
612        let f = f
613            .splice_fn1_borrow_ctx(&OperatorContext::<L, B>::new(&self.location))
614            .into();
615        Optional::new(
616            self.location.clone(),
617            HydroNode::Filter {
618                f,
619                input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
620                metadata: self.location.new_node_metadata(Self::collection_kind()),
621            },
622        )
623    }
624
625    /// An operator that both filters and maps. It yields only the value if the supplied
626    /// closure `f` returns `Some(value)`.
627    ///
628    /// If the optional is empty, the output optional is also empty. If the optional contains
629    /// a value and the closure returns `Some(new_value)`, the output optional contains `new_value`.
630    /// If the closure returns `None`, the output optional is empty.
631    ///
632    /// # Example
633    /// ```rust
634    /// # #[cfg(feature = "deploy")] {
635    /// # use hydro_lang::prelude::*;
636    /// # use futures::StreamExt;
637    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
638    /// let tick = process.tick();
639    /// let optional = tick.optional_first_tick(q!("42"));
640    /// optional
641    ///     .filter_map(q!(|s| s.parse::<i32>().ok()))
642    ///     .all_ticks()
643    /// # }, |mut stream| async move {
644    /// // 42
645    /// # assert_eq!(stream.next().await.unwrap(), 42);
646    /// # }));
647    /// # }
648    /// ```
649    pub fn filter_map<U, F>(
650        self,
651        f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>>,
652    ) -> Optional<U, L, B>
653    where
654        F: Fn(T) -> Option<U> + 'a,
655    {
656        let f = f
657            .splice_fn1_ctx(&OperatorContext::<L, B>::new(&self.location))
658            .into();
659        Optional::new(
660            self.location.clone(),
661            HydroNode::FilterMap {
662                f,
663                input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
664                metadata: self
665                    .location
666                    .new_node_metadata(Optional::<U, L, B>::collection_kind()),
667            },
668        )
669    }
670
671    /// Combines this singleton with another [`Singleton`] or [`Optional`] by tupling their values.
672    ///
673    /// If the other value is a [`Optional`], the output will be non-null only if the argument is
674    /// non-null. This is useful for combining several pieces of state together.
675    ///
676    /// # Example
677    /// ```rust
678    /// # #[cfg(feature = "deploy")] {
679    /// # use hydro_lang::prelude::*;
680    /// # use futures::StreamExt;
681    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
682    /// let tick = process.tick();
683    /// let numbers = process
684    ///   .source_iter(q!(vec![123, 456, 789]))
685    ///   .batch(&tick, nondet!(/** test */));
686    /// let min = numbers.clone().min(); // Optional
687    /// let max = numbers.max(); // Optional
688    /// min.zip(max).all_ticks()
689    /// # }, |mut stream| async move {
690    /// // [(123, 789)]
691    /// # for w in vec![(123, 789)] {
692    /// #     assert_eq!(stream.next().await.unwrap(), w);
693    /// # }
694    /// # }));
695    /// # }
696    /// ```
697    pub fn zip<O>(self, other: impl Into<Optional<O, L, B>>) -> Optional<(T, O), L, B>
698    where
699        B: IsBounded,
700    {
701        let other: Optional<O, L, B> = other.into();
702        check_matching_location(&self.location, &other.location);
703
704        if L::is_top_level()
705            && let Some(tick) = self.location.try_tick()
706        {
707            let self_location = self.location().clone();
708            let out = zip_inside_tick(
709                self.snapshot(&tick, nondet!(/** eventually stabilizes */)),
710                other.snapshot(&tick, nondet!(/** eventually stabilizes */)),
711            )
712            .latest();
713
714            Optional::new(self_location, out.ir_node.replace(HydroNode::Placeholder))
715        } else {
716            zip_inside_tick(self, other)
717        }
718    }
719
720    /// Passes through `self` when it has a value, otherwise passes through `other`.
721    ///
722    /// Like [`Option::or`], this is helpful for defining a fallback for an [`Optional`], when the
723    /// fallback itself is an [`Optional`]. If the fallback is a [`Singleton`], you can use
724    /// [`Optional::unwrap_or`] to ensure that the output is always non-null.
725    ///
726    /// If the inputs are [`Unbounded`], the output will be asynchronously updated as the contents
727    /// of the inputs change (including to/from null states).
728    ///
729    /// # Example
730    /// ```rust
731    /// # #[cfg(feature = "deploy")] {
732    /// # use hydro_lang::prelude::*;
733    /// # use futures::StreamExt;
734    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
735    /// let tick = process.tick();
736    /// // ticks are lazy by default, forces the second tick to run
737    /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
738    ///
739    /// let some_first_tick = tick.optional_first_tick(q!(123));
740    /// let some_second_tick = tick.optional_first_tick(q!(456)).defer_tick();
741    /// some_first_tick.or(some_second_tick).all_ticks()
742    /// # }, |mut stream| async move {
743    /// // [123 /* first tick */, 456 /* second tick */]
744    /// # for w in vec![123, 456] {
745    /// #     assert_eq!(stream.next().await.unwrap(), w);
746    /// # }
747    /// # }));
748    /// # }
749    /// ```
750    pub fn or(self, other: Optional<T, L, B>) -> Optional<T, L, B> {
751        check_matching_location(&self.location, &other.location);
752
753        if L::is_top_level()
754            && !B::BOUNDED // only if unbounded we need to use a tick
755            && let Some(tick) = self.location.try_tick()
756        {
757            let self_location = self.location().clone();
758            let out = or_inside_tick(
759                self.snapshot(&tick, nondet!(/** eventually stabilizes */)),
760                other.snapshot(&tick, nondet!(/** eventually stabilizes */)),
761            )
762            .latest();
763
764            Optional::new(self_location, out.ir_node.replace(HydroNode::Placeholder))
765        } else {
766            Optional::new(
767                self.location.clone(),
768                HydroNode::ChainFirst {
769                    first: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
770                    second: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
771                    metadata: self.location.new_node_metadata(Self::collection_kind()),
772                },
773            )
774        }
775    }
776
777    /// Gets the contents of `self` when it has a value, otherwise passes through `other`.
778    ///
779    /// Like [`Option::unwrap_or`], this is helpful for defining a fallback for an [`Optional`].
780    /// If the fallback is not always defined (an [`Optional`]), you can use [`Optional::or`].
781    ///
782    /// If the inputs are [`Unbounded`], the output will be asynchronously updated as the contents
783    /// of the inputs change (including to/from null states).
784    ///
785    /// # Example
786    /// ```rust
787    /// # #[cfg(feature = "deploy")] {
788    /// # use hydro_lang::prelude::*;
789    /// # use futures::StreamExt;
790    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
791    /// let tick = process.tick();
792    /// // ticks are lazy by default, forces the later ticks to run
793    /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
794    ///
795    /// let some_first_tick = tick.optional_first_tick(q!(123));
796    /// some_first_tick
797    ///     .unwrap_or(tick.singleton(q!(456)))
798    ///     .all_ticks()
799    /// # }, |mut stream| async move {
800    /// // [123 /* first tick */, 456 /* second tick */, 456 /* third tick */, 456, ...]
801    /// # for w in vec![123, 456, 456, 456] {
802    /// #     assert_eq!(stream.next().await.unwrap(), w);
803    /// # }
804    /// # }));
805    /// # }
806    /// ```
807    pub fn unwrap_or(self, other: Singleton<T, L, B>) -> Singleton<T, L, B> {
808        let res_option = self.or(other.into());
809        Singleton::new(
810            res_option.location.clone(),
811            HydroNode::Cast {
812                inner: Box::new(res_option.ir_node.replace(HydroNode::Placeholder)),
813                metadata: res_option
814                    .location
815                    .new_node_metadata(Singleton::<T, L, B>::collection_kind()),
816            },
817        )
818    }
819
820    /// Gets the contents of `self` when it has a value, otherwise returns the default value of `T`.
821    ///
822    /// Like [`Option::unwrap_or_default`], this is helpful for defining a fallback for an
823    /// [`Optional`] when the default value of the type is a suitable fallback.
824    ///
825    /// # Example
826    /// ```rust
827    /// # #[cfg(feature = "deploy")] {
828    /// # use hydro_lang::prelude::*;
829    /// # use futures::StreamExt;
830    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
831    /// let tick = process.tick();
832    /// // ticks are lazy by default, forces the later ticks to run
833    /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
834    ///
835    /// let some_first_tick = tick.optional_first_tick(q!(123i32));
836    /// some_first_tick.unwrap_or_default().all_ticks()
837    /// # }, |mut stream| async move {
838    /// // [123 /* first tick */, 0 /* second tick */, 0 /* third tick */, 0, ...]
839    /// # for w in vec![123, 0, 0, 0] {
840    /// #     assert_eq!(stream.next().await.unwrap(), w);
841    /// # }
842    /// # }));
843    /// # }
844    /// ```
845    pub fn unwrap_or_default(self) -> Singleton<T, L, B>
846    where
847        T: Default + Clone,
848    {
849        self.into_singleton().map(q!(|v| v.unwrap_or_default()))
850    }
851
852    /// Converts this optional into a [`Singleton`] with a Rust [`Option`] as its contents.
853    ///
854    /// Useful for writing custom Rust code that needs to interact with both the null and non-null
855    /// states of the [`Optional`]. When possible, you should use the native APIs on [`Optional`]
856    /// so that Hydro can skip any computation on null values.
857    ///
858    /// # Example
859    /// ```rust
860    /// # #[cfg(feature = "deploy")] {
861    /// # use hydro_lang::prelude::*;
862    /// # use futures::StreamExt;
863    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
864    /// let tick = process.tick();
865    /// // ticks are lazy by default, forces the later ticks to run
866    /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
867    ///
868    /// let some_first_tick = tick.optional_first_tick(q!(123));
869    /// some_first_tick.into_singleton().all_ticks()
870    /// # }, |mut stream| async move {
871    /// // [Some(123) /* first tick */, None /* second tick */, None /* third tick */, None, ...]
872    /// # for w in vec![Some(123), None, None, None] {
873    /// #     assert_eq!(stream.next().await.unwrap(), w);
874    /// # }
875    /// # }));
876    /// # }
877    /// ```
878    pub fn into_singleton(self) -> Singleton<Option<T>, L, B>
879    where
880        T: Clone,
881    {
882        let none: syn::Expr = parse_quote!(::std::option::Option::None);
883
884        let none_singleton = Singleton::new(
885            self.location.clone(),
886            HydroNode::SingletonSource {
887                value: none.into(),
888                first_tick_only: false,
889                metadata: self
890                    .location
891                    .new_node_metadata(Singleton::<Option<T>, L, B>::collection_kind()),
892            },
893        );
894
895        self.map(q!(|v| Some(v))).unwrap_or(none_singleton)
896    }
897
898    /// Returns a [`Singleton`] containing `true` if this optional has a value, `false` otherwise.
899    ///
900    /// # Example
901    /// ```rust
902    /// # #[cfg(feature = "deploy")] {
903    /// # use hydro_lang::prelude::*;
904    /// # use futures::StreamExt;
905    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
906    /// let tick = process.tick();
907    /// // ticks are lazy by default, forces the second tick to run
908    /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
909    ///
910    /// let some_first_tick = tick.optional_first_tick(q!(42));
911    /// some_first_tick.is_some().all_ticks()
912    /// # }, |mut stream| async move {
913    /// // [true /* first tick */, false /* second tick */, ...]
914    /// # for w in vec![true, false] {
915    /// #     assert_eq!(stream.next().await.unwrap(), w);
916    /// # }
917    /// # }));
918    /// # }
919    /// ```
920    #[expect(clippy::wrong_self_convention, reason = "Stream naming")]
921    pub fn is_some(self) -> Singleton<bool, L, B> {
922        self.map(q!(|_| ()))
923            .into_singleton()
924            .map(q!(|o| o.is_some()))
925    }
926
927    /// Returns a [`Singleton`] containing `true` if this optional is null, `false` otherwise.
928    ///
929    /// # Example
930    /// ```rust
931    /// # #[cfg(feature = "deploy")] {
932    /// # use hydro_lang::prelude::*;
933    /// # use futures::StreamExt;
934    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
935    /// let tick = process.tick();
936    /// // ticks are lazy by default, forces the second tick to run
937    /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
938    ///
939    /// let some_first_tick = tick.optional_first_tick(q!(42));
940    /// some_first_tick.is_none().all_ticks()
941    /// # }, |mut stream| async move {
942    /// // [false /* first tick */, true /* second tick */, ...]
943    /// # for w in vec![false, true] {
944    /// #     assert_eq!(stream.next().await.unwrap(), w);
945    /// # }
946    /// # }));
947    /// # }
948    /// ```
949    #[expect(clippy::wrong_self_convention, reason = "Stream naming")]
950    pub fn is_none(self) -> Singleton<bool, L, B> {
951        self.map(q!(|_| ()))
952            .into_singleton()
953            .map(q!(|o| o.is_none()))
954    }
955
956    /// Returns a [`Singleton`] containing `true` if both optionals are non-null and their
957    /// values are equal, `false` otherwise (including when either is null).
958    ///
959    /// # Example
960    /// ```rust
961    /// # #[cfg(feature = "deploy")] {
962    /// # use hydro_lang::prelude::*;
963    /// # use futures::StreamExt;
964    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
965    /// let tick = process.tick();
966    /// // ticks are lazy by default, forces the second tick to run
967    /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
968    ///
969    /// let a = tick.optional_first_tick(q!(5)); // Some(5), None
970    /// let b = tick.optional_first_tick(q!(5)); // Some(5), None
971    /// a.is_some_and_equals(b).all_ticks()
972    /// # }, |mut stream| async move {
973    /// // [true, false]
974    /// # for w in vec![true, false] {
975    /// #     assert_eq!(stream.next().await.unwrap(), w);
976    /// # }
977    /// # }));
978    /// # }
979    /// ```
980    #[expect(clippy::wrong_self_convention, reason = "Stream naming")]
981    pub fn is_some_and_equals(self, other: Optional<T, L, B>) -> Singleton<bool, L, B>
982    where
983        T: PartialEq + Clone,
984        B: IsBounded,
985    {
986        self.into_singleton()
987            .zip(other.into_singleton())
988            .map(q!(|(a, b)| a.is_some() && a == b))
989    }
990
991    /// An operator which allows you to "name" a `HydroNode`.
992    /// This is only used for testing, to correlate certain `HydroNode`s with IDs.
993    pub fn ir_node_named(self, name: &str) -> Optional<T, L, B> {
994        {
995            let mut node = self.ir_node.borrow_mut();
996            let metadata = node.metadata_mut();
997            metadata.tag = Some(name.to_owned());
998        }
999        self
1000    }
1001
1002    /// Strengthens the boundedness guarantee to `Bounded`, given that `B: IsBounded`, which
1003    /// implies that `B == Bounded`.
1004    pub fn make_bounded(self) -> Optional<T, L, Bounded>
1005    where
1006        B: IsBounded,
1007    {
1008        Optional::new(
1009            self.location.clone(),
1010            self.ir_node.replace(HydroNode::Placeholder),
1011        )
1012    }
1013
1014    /// Clones this bounded optional into a tick, returning a optional that has the
1015    /// same value as the outer optional. Because the outer optional is bounded, this
1016    /// is deterministic because there is only a single immutable version.
1017    pub fn clone_into_tick(self, tick: &Tick<L>) -> Optional<T, Tick<L>, Bounded>
1018    where
1019        B: IsBounded,
1020        T: Clone,
1021    {
1022        // TODO(shadaj): avoid printing simulator logs for this snapshot
1023        let inner = self.snapshot(
1024            tick,
1025            nondet!(/** bounded top-level optional so deterministic */),
1026        );
1027        Optional::new(tick.clone(), inner.ir_node.replace(HydroNode::Placeholder))
1028    }
1029
1030    /// Converts this optional into a [`Stream`] containing a single element, the value, if it is
1031    /// non-null. Otherwise, the stream is empty.
1032    ///
1033    /// # Example
1034    /// ```rust
1035    /// # #[cfg(feature = "deploy")] {
1036    /// # use hydro_lang::prelude::*;
1037    /// # use futures::StreamExt;
1038    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1039    /// # let tick = process.tick();
1040    /// # // ticks are lazy by default, forces the second tick to run
1041    /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1042    /// # let batch_first_tick = process
1043    /// #   .source_iter(q!(vec![]))
1044    /// #   .batch(&tick, nondet!(/** test */));
1045    /// # let batch_second_tick = process
1046    /// #   .source_iter(q!(vec![123, 456]))
1047    /// #   .batch(&tick, nondet!(/** test */))
1048    /// #   .defer_tick(); // appears on the second tick
1049    /// # let input_batch = batch_first_tick.chain(batch_second_tick);
1050    /// input_batch // first tick: [], second tick: [123, 456]
1051    ///     .clone()
1052    ///     .max()
1053    ///     .into_stream()
1054    ///     .chain(input_batch)
1055    ///     .all_ticks()
1056    /// # }, |mut stream| async move {
1057    /// // [456, 123, 456]
1058    /// # for w in vec![456, 123, 456] {
1059    /// #     assert_eq!(stream.next().await.unwrap(), w);
1060    /// # }
1061    /// # }));
1062    /// # }
1063    /// ```
1064    pub fn into_stream(self) -> Stream<T, L, Bounded, TotalOrder, ExactlyOnce>
1065    where
1066        B: IsBounded,
1067    {
1068        Stream::new(
1069            self.location.clone(),
1070            HydroNode::Cast {
1071                inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1072                metadata: self.location.new_node_metadata(Stream::<
1073                    T,
1074                    Tick<L>,
1075                    Bounded,
1076                    TotalOrder,
1077                    ExactlyOnce,
1078                >::collection_kind()),
1079            },
1080        )
1081    }
1082
1083    /// Filters this optional, passing through the value if the boolean signal is `true`,
1084    /// otherwise the output is null.
1085    ///
1086    /// # Example
1087    /// ```rust
1088    /// # #[cfg(feature = "deploy")] {
1089    /// # use hydro_lang::prelude::*;
1090    /// # use futures::StreamExt;
1091    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1092    /// let tick = process.tick();
1093    /// // ticks are lazy by default, forces the second tick to run
1094    /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1095    ///
1096    /// let some_first_tick = tick.optional_first_tick(q!(()));
1097    /// let signal = some_first_tick.is_some(); // true on first tick, false on second
1098    /// let batch_first_tick = process
1099    ///   .source_iter(q!(vec![456]))
1100    ///   .batch(&tick, nondet!(/** test */));
1101    /// let batch_second_tick = process
1102    ///   .source_iter(q!(vec![789]))
1103    ///   .batch(&tick, nondet!(/** test */))
1104    ///   .defer_tick();
1105    /// batch_first_tick.chain(batch_second_tick).first()
1106    ///   .filter_if(signal)
1107    ///   .unwrap_or(tick.singleton(q!(0)))
1108    ///   .all_ticks()
1109    /// # }, |mut stream| async move {
1110    /// // [456, 0]
1111    /// # for w in vec![456, 0] {
1112    /// #     assert_eq!(stream.next().await.unwrap(), w);
1113    /// # }
1114    /// # }));
1115    /// # }
1116    /// ```
1117    pub fn filter_if(self, signal: Singleton<bool, L, B>) -> Optional<T, L, B>
1118    where
1119        B: IsBounded,
1120    {
1121        self.zip(signal.filter(q!(|b| *b))).map(q!(|(d, _)| d))
1122    }
1123
1124    /// Filters this optional, passing through the optional value if it is non-null **and** the
1125    /// argument (a [`Bounded`] [`Optional`]`) is non-null, otherwise the output is null.
1126    ///
1127    /// Useful for conditionally processing, such as only emitting an optional's value outside
1128    /// a tick if some other condition is satisfied.
1129    ///
1130    /// # Example
1131    /// ```rust
1132    /// # #[cfg(feature = "deploy")] {
1133    /// # use hydro_lang::prelude::*;
1134    /// # use futures::StreamExt;
1135    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1136    /// let tick = process.tick();
1137    /// // ticks are lazy by default, forces the second tick to run
1138    /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1139    ///
1140    /// let batch_first_tick = process
1141    ///   .source_iter(q!(vec![]))
1142    ///   .batch(&tick, nondet!(/** test */));
1143    /// let batch_second_tick = process
1144    ///   .source_iter(q!(vec![456]))
1145    ///   .batch(&tick, nondet!(/** test */))
1146    ///   .defer_tick(); // appears on the second tick
1147    /// let some_on_first_tick = tick.optional_first_tick(q!(()));
1148    /// batch_first_tick.chain(batch_second_tick).first()
1149    ///   .filter_if_some(some_on_first_tick)
1150    ///   .unwrap_or(tick.singleton(q!(789)))
1151    ///   .all_ticks()
1152    /// # }, |mut stream| async move {
1153    /// // [789, 789]
1154    /// # for w in vec![789, 789] {
1155    /// #     assert_eq!(stream.next().await.unwrap(), w);
1156    /// # }
1157    /// # }));
1158    /// # }
1159    /// ```
1160    #[deprecated(note = "use `filter_if` with `Optional::is_some()` instead")]
1161    pub fn filter_if_some<U>(self, signal: Optional<U, L, B>) -> Optional<T, L, B>
1162    where
1163        B: IsBounded,
1164    {
1165        self.filter_if(signal.is_some())
1166    }
1167
1168    /// Filters this optional, passing through the optional value if it is non-null **and** the
1169    /// argument (a [`Bounded`] [`Optional`]`) is _null_, otherwise the output is null.
1170    ///
1171    /// Useful for conditionally processing, such as only emitting an optional's value outside
1172    /// a tick if some other condition is satisfied.
1173    ///
1174    /// # Example
1175    /// ```rust
1176    /// # #[cfg(feature = "deploy")] {
1177    /// # use hydro_lang::prelude::*;
1178    /// # use futures::StreamExt;
1179    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1180    /// let tick = process.tick();
1181    /// // ticks are lazy by default, forces the second tick to run
1182    /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1183    ///
1184    /// let batch_first_tick = process
1185    ///   .source_iter(q!(vec![]))
1186    ///   .batch(&tick, nondet!(/** test */));
1187    /// let batch_second_tick = process
1188    ///   .source_iter(q!(vec![456]))
1189    ///   .batch(&tick, nondet!(/** test */))
1190    ///   .defer_tick(); // appears on the second tick
1191    /// let some_on_first_tick = tick.optional_first_tick(q!(()));
1192    /// batch_first_tick.chain(batch_second_tick).first()
1193    ///   .filter_if_none(some_on_first_tick)
1194    ///   .unwrap_or(tick.singleton(q!(789)))
1195    ///   .all_ticks()
1196    /// # }, |mut stream| async move {
1197    /// // [789, 789]
1198    /// # for w in vec![789, 456] {
1199    /// #     assert_eq!(stream.next().await.unwrap(), w);
1200    /// # }
1201    /// # }));
1202    /// # }
1203    /// ```
1204    #[deprecated(note = "use `filter_if` with `!Optional::is_some()` instead")]
1205    pub fn filter_if_none<U>(self, other: Optional<U, L, B>) -> Optional<T, L, B>
1206    where
1207        B: IsBounded,
1208    {
1209        self.filter_if(other.is_none())
1210    }
1211
1212    /// If `self` is null, emits a null optional, but if it non-null, emits `value`.
1213    ///
1214    /// Useful for gating the release of a [`Singleton`] on a condition of the [`Optional`]
1215    /// having a value, such as only releasing a piece of state if the node is the leader.
1216    ///
1217    /// # Example
1218    /// ```rust
1219    /// # #[cfg(feature = "deploy")] {
1220    /// # use hydro_lang::prelude::*;
1221    /// # use futures::StreamExt;
1222    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1223    /// let tick = process.tick();
1224    /// // ticks are lazy by default, forces the second tick to run
1225    /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1226    ///
1227    /// let some_on_first_tick = tick.optional_first_tick(q!(()));
1228    /// some_on_first_tick
1229    ///     .if_some_then(tick.singleton(q!(456)))
1230    ///     .unwrap_or(tick.singleton(q!(123)))
1231    /// # .all_ticks()
1232    /// # }, |mut stream| async move {
1233    /// // 456 (first tick) ~> 123 (second tick onwards)
1234    /// # for w in vec![456, 123, 123] {
1235    /// #     assert_eq!(stream.next().await.unwrap(), w);
1236    /// # }
1237    /// # }));
1238    /// # }
1239    /// ```
1240    #[deprecated(note = "use `filter_if` with `Optional::is_some()` instead")]
1241    pub fn if_some_then<U>(self, value: Singleton<U, L, B>) -> Optional<U, L, B>
1242    where
1243        B: IsBounded,
1244    {
1245        value.filter_if(self.is_some())
1246    }
1247}
1248
1249impl<'a, K, V, L, B: Boundedness> Optional<(K, V), L, B>
1250where
1251    L: Location<'a>,
1252{
1253    /// Converts this optional into a [`KeyedSingleton`] containing a single entry with the
1254    /// key-value pair of this [`Optional`].
1255    ///
1256    /// If this [`Optional`] is [`Bounded`], the [`KeyedSingleton`] will be [`Bounded`] as well
1257    /// if it is [`Unbounded`], the [`KeyedSingleton`] will be [`Unbounded`], which means that
1258    /// the entry will be updated and appear / disappear according to the state of the
1259    /// [`Optional`].
1260    pub fn into_keyed_singleton(self) -> KeyedSingleton<K, V, L, B> {
1261        KeyedSingleton::new(
1262            self.location.clone(),
1263            HydroNode::Cast {
1264                inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1265                metadata: self
1266                    .location
1267                    .new_node_metadata(KeyedSingleton::<K, V, L, B>::collection_kind()),
1268            },
1269        )
1270    }
1271}
1272
1273impl<'a, T, L, B: Boundedness> Optional<T, Atomic<L>, B>
1274where
1275    L: Location<'a>,
1276{
1277    /// Returns an optional value corresponding to the latest snapshot of the optional
1278    /// being atomically processed. The snapshot at tick `t + 1` is guaranteed to include
1279    /// at least all relevant data that contributed to the snapshot at tick `t`. Furthermore,
1280    /// all snapshots of this optional into the atomic-associated tick will observe the
1281    /// same value each tick.
1282    ///
1283    /// # Non-Determinism
1284    /// Because this picks a snapshot of a optional whose value is continuously changing,
1285    /// the output optional has a non-deterministic value since the snapshot can be at an
1286    /// arbitrary point in time.
1287    pub fn snapshot_atomic<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1288        self,
1289        tick: &Tick<L2>,
1290        _nondet: NonDet,
1291    ) -> Optional<T, Tick<L::DropConsistency>, Bounded> {
1292        Optional::new(
1293            tick.drop_consistency(),
1294            HydroNode::Batch {
1295                inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1296                metadata: tick
1297                    .new_node_metadata(Optional::<T, Tick<L>, Bounded>::collection_kind()),
1298            },
1299        )
1300    }
1301}
1302
1303impl<'a, T, L, B: Boundedness> Optional<T, L, B>
1304where
1305    L: Location<'a>,
1306{
1307    /// Given a tick, returns a optional value corresponding to a snapshot of the optional
1308    /// as of that tick. The snapshot at tick `t + 1` is guaranteed to include at least all
1309    /// relevant data that contributed to the snapshot at tick `t`.
1310    ///
1311    /// # Non-Determinism
1312    /// Because this picks a snapshot of a optional whose value is continuously changing,
1313    /// the output optional has a non-deterministic value since the snapshot can be at an
1314    /// arbitrary point in time.
1315    pub fn snapshot<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1316        self,
1317        tick: &Tick<L2>,
1318        _nondet: NonDet,
1319    ) -> Optional<T, Tick<L::DropConsistency>, Bounded> {
1320        assert_eq!(Location::id(tick.outer()), Location::id(&self.location));
1321        Optional::new(
1322            tick.drop_consistency(),
1323            HydroNode::Batch {
1324                inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1325                metadata: tick
1326                    .new_node_metadata(Optional::<T, Tick<L>, Bounded>::collection_kind()),
1327            },
1328        )
1329    }
1330
1331    /// Eagerly samples the optional as fast as possible, returning a stream of snapshots
1332    /// with order corresponding to increasing prefixes of data contributing to the optional.
1333    ///
1334    /// # Non-Determinism
1335    /// At runtime, the optional will be arbitrarily sampled as fast as possible, but due
1336    /// to non-deterministic batching and arrival of inputs, the output stream is
1337    /// non-deterministic.
1338    pub fn sample_eager(
1339        self,
1340        nondet: NonDet,
1341    ) -> Stream<T, L::DropConsistency, Unbounded, TotalOrder, AtLeastOnce> {
1342        let tick = self.location.tick();
1343        self.snapshot(&tick, nondet).all_ticks().weaken_retries()
1344    }
1345
1346    /// Given a time interval, returns a stream corresponding to snapshots of the optional
1347    /// value taken at various points in time. Because the input optional may be
1348    /// [`Unbounded`], there are no guarantees on what these snapshots are other than they
1349    /// represent the value of the optional given some prefix of the streams leading up to
1350    /// it.
1351    ///
1352    /// # Non-Determinism
1353    /// The output stream is non-deterministic in which elements are sampled, since this
1354    /// is controlled by a clock.
1355    #[cfg(feature = "tokio")]
1356    pub fn sample_every(
1357        self,
1358        interval: impl QuotedWithContext<'a, std::time::Duration, L> + Copy + 'a,
1359        nondet: NonDet,
1360    ) -> Stream<T, L::DropConsistency, Unbounded, TotalOrder, AtLeastOnce>
1361    where
1362        L: TopLevel<'a>,
1363    {
1364        let samples = self.location.source_interval(interval);
1365        let tick = self.location.tick();
1366
1367        self.snapshot(&tick, nondet)
1368            .filter_if(samples.batch(&tick, nondet).first().is_some())
1369            .all_ticks()
1370            .weaken_retries()
1371    }
1372}
1373
1374impl<'a, T, L> Optional<T, Tick<L>, Bounded>
1375where
1376    L: Location<'a>,
1377{
1378    /// Asynchronously yields the value of this singleton outside the tick as an unbounded stream,
1379    /// which will stream the value computed in _each_ tick as a separate stream element (skipping
1380    /// null values).
1381    ///
1382    /// Unlike [`Optional::latest`], the value computed in each tick is emitted separately,
1383    /// producing one element in the output for each (non-null) tick. This is useful for batched
1384    /// computations, where the results from each tick must be combined together.
1385    ///
1386    /// # Example
1387    /// ```rust
1388    /// # #[cfg(feature = "deploy")] {
1389    /// # use hydro_lang::prelude::*;
1390    /// # use futures::StreamExt;
1391    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1392    /// # let tick = process.tick();
1393    /// # // ticks are lazy by default, forces the second tick to run
1394    /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1395    /// # let batch_first_tick = process
1396    /// #   .source_iter(q!(vec![]))
1397    /// #   .batch(&tick, nondet!(/** test */));
1398    /// # let batch_second_tick = process
1399    /// #   .source_iter(q!(vec![1, 2, 3]))
1400    /// #   .batch(&tick, nondet!(/** test */))
1401    /// #   .defer_tick(); // appears on the second tick
1402    /// # let input_batch = batch_first_tick.chain(batch_second_tick);
1403    /// input_batch // first tick: [], second tick: [1, 2, 3]
1404    ///     .max()
1405    ///     .all_ticks()
1406    /// # }, |mut stream| async move {
1407    /// // [3]
1408    /// # for w in vec![3] {
1409    /// #     assert_eq!(stream.next().await.unwrap(), w);
1410    /// # }
1411    /// # }));
1412    /// # }
1413    /// ```
1414    pub fn all_ticks(self) -> Stream<T, L, Unbounded, TotalOrder, ExactlyOnce> {
1415        self.into_stream().all_ticks()
1416    }
1417
1418    /// Synchronously yields the value of this optional outside the tick as an unbounded stream,
1419    /// which will stream the value computed in _each_ tick as a separate stream element.
1420    ///
1421    /// Unlike [`Optional::all_ticks`], this preserves synchronous execution, as the output stream
1422    /// is emitted in an [`Atomic`] context that will process elements synchronously with the input
1423    /// optional's [`Tick`] context.
1424    pub fn all_ticks_atomic(self) -> Stream<T, Atomic<L>, Unbounded, TotalOrder, ExactlyOnce> {
1425        self.into_stream().all_ticks_atomic()
1426    }
1427
1428    /// Asynchronously yields this optional outside the tick as an unbounded optional, which will
1429    /// be asynchronously updated with the latest value of the optional inside the tick, including
1430    /// whether the optional is null or not.
1431    ///
1432    /// This converts a bounded value _inside_ a tick into an asynchronous value outside the
1433    /// tick that tracks the inner value. This is useful for getting the value as of the
1434    /// "most recent" tick, but note that updates are propagated asynchronously outside the tick.
1435    ///
1436    /// # Example
1437    /// ```rust
1438    /// # #[cfg(feature = "deploy")] {
1439    /// # use hydro_lang::prelude::*;
1440    /// # use futures::StreamExt;
1441    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1442    /// # let tick = process.tick();
1443    /// # // ticks are lazy by default, forces the second tick to run
1444    /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1445    /// # let batch_first_tick = process
1446    /// #   .source_iter(q!(vec![]))
1447    /// #   .batch(&tick, nondet!(/** test */));
1448    /// # let batch_second_tick = process
1449    /// #   .source_iter(q!(vec![1, 2, 3]))
1450    /// #   .batch(&tick, nondet!(/** test */))
1451    /// #   .defer_tick(); // appears on the second tick
1452    /// # let input_batch = batch_first_tick.chain(batch_second_tick);
1453    /// input_batch // first tick: [], second tick: [1, 2, 3]
1454    ///     .max()
1455    ///     .latest()
1456    /// # .into_singleton()
1457    /// # .sample_eager(nondet!(/** test */))
1458    /// # }, |mut stream| async move {
1459    /// // asynchronously changes from None ~> 3
1460    /// # for w in vec![None, Some(3)] {
1461    /// #     assert_eq!(stream.next().await.unwrap(), w);
1462    /// # }
1463    /// # }));
1464    /// # }
1465    /// ```
1466    pub fn latest(self) -> Optional<T, L, Unbounded> {
1467        Optional::new(
1468            self.location.outer().clone(),
1469            HydroNode::YieldConcat {
1470                inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1471                metadata: self
1472                    .location
1473                    .outer()
1474                    .new_node_metadata(Optional::<T, L, Unbounded>::collection_kind()),
1475            },
1476        )
1477    }
1478
1479    /// Synchronously yields this optional outside the tick as an unbounded optional, which will
1480    /// be updated with the latest value of the optional inside the tick.
1481    ///
1482    /// Unlike [`Optional::latest`], this preserves synchronous execution, as the output optional
1483    /// is emitted in an [`Atomic`] context that will process elements synchronously with the input
1484    /// optional's [`Tick`] context.
1485    pub fn latest_atomic(self) -> Optional<T, Atomic<L>, Unbounded> {
1486        let out_location = Atomic {
1487            tick: self.location.clone(),
1488        };
1489
1490        Optional::new(
1491            out_location.clone(),
1492            HydroNode::YieldConcat {
1493                inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1494                metadata: out_location
1495                    .new_node_metadata(Optional::<T, Atomic<L>, Unbounded>::collection_kind()),
1496            },
1497        )
1498    }
1499
1500    /// Shifts the state in `self` to the **next tick**, so that the returned optional at tick `T`
1501    /// always has the state of `self` at tick `T - 1`.
1502    ///
1503    /// At tick `0`, the output optional is null, since there is no previous tick.
1504    ///
1505    /// This operator enables stateful iterative processing with ticks, by sending data from one
1506    /// tick to the next. For example, you can use it to compare state across consecutive batches.
1507    ///
1508    /// # Example
1509    /// ```rust
1510    /// # #[cfg(feature = "deploy")] {
1511    /// # use hydro_lang::prelude::*;
1512    /// # use futures::StreamExt;
1513    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1514    /// let tick = process.tick();
1515    /// // ticks are lazy by default, forces the second tick to run
1516    /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1517    ///
1518    /// let batch_first_tick = process
1519    ///   .source_iter(q!(vec![1, 2]))
1520    ///   .batch(&tick, nondet!(/** test */));
1521    /// let batch_second_tick = process
1522    ///   .source_iter(q!(vec![3, 4]))
1523    ///   .batch(&tick, nondet!(/** test */))
1524    ///   .defer_tick(); // appears on the second tick
1525    /// let current_tick_sum = batch_first_tick.chain(batch_second_tick)
1526    ///   .reduce(q!(|state, v| *state += v));
1527    ///
1528    /// current_tick_sum.clone().into_singleton().zip(
1529    ///   current_tick_sum.defer_tick().into_singleton() // state from previous tick
1530    /// ).all_ticks()
1531    /// # }, |mut stream| async move {
1532    /// // [(Some(3), None) /* first tick */, (Some(7), Some(3)) /* second tick */]
1533    /// # for w in vec![(Some(3), None), (Some(7), Some(3))] {
1534    /// #     assert_eq!(stream.next().await.unwrap(), w);
1535    /// # }
1536    /// # }));
1537    /// # }
1538    /// ```
1539    pub fn defer_tick(self) -> Optional<T, Tick<L>, Bounded> {
1540        Optional::new(
1541            self.location.clone(),
1542            HydroNode::DeferTick {
1543                input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1544                metadata: self.location.new_node_metadata(Self::collection_kind()),
1545            },
1546        )
1547    }
1548}
1549
1550#[cfg(test)]
1551mod tests {
1552    #[cfg(feature = "deploy")]
1553    use futures::StreamExt;
1554    #[cfg(feature = "deploy")]
1555    use hydro_deploy::Deployment;
1556    #[cfg(any(feature = "deploy", feature = "sim"))]
1557    use stageleft::q;
1558
1559    #[cfg(feature = "deploy")]
1560    use super::Optional;
1561    #[cfg(any(feature = "deploy", feature = "sim"))]
1562    use crate::compile::builder::FlowBuilder;
1563    #[cfg(any(feature = "deploy", feature = "sim"))]
1564    use crate::location::Location;
1565    #[cfg(feature = "deploy")]
1566    use crate::nondet::nondet;
1567
1568    #[cfg(feature = "deploy")]
1569    #[tokio::test]
1570    async fn optional_or_cardinality() {
1571        let mut deployment = Deployment::new();
1572
1573        let mut flow = FlowBuilder::new();
1574        let node = flow.process::<()>();
1575        let external = flow.external::<()>();
1576
1577        let node_tick = node.tick();
1578        let tick_singleton = node_tick.singleton(q!(123));
1579        let tick_optional_inhabited: Optional<_, _, _> = tick_singleton.into();
1580        let counts = tick_optional_inhabited
1581            .clone()
1582            .or(tick_optional_inhabited)
1583            .into_stream()
1584            .count()
1585            .all_ticks()
1586            .send_bincode_external(&external);
1587
1588        let nodes = flow
1589            .with_process(&node, deployment.Localhost())
1590            .with_external(&external, deployment.Localhost())
1591            .deploy(&mut deployment);
1592
1593        deployment.deploy().await.unwrap();
1594
1595        let mut external_out = nodes.connect(counts).await;
1596
1597        deployment.start().await.unwrap();
1598
1599        assert_eq!(external_out.next().await.unwrap(), 1);
1600    }
1601
1602    #[cfg(feature = "deploy")]
1603    #[tokio::test]
1604    async fn into_singleton_top_level_none_cardinality() {
1605        let mut deployment = Deployment::new();
1606
1607        let mut flow = FlowBuilder::new();
1608        let node = flow.process::<()>();
1609        let external = flow.external::<()>();
1610
1611        let node_tick = node.tick();
1612        let top_level_none = node.singleton(q!(123)).filter(q!(|_| false));
1613        let into_singleton = top_level_none.into_singleton();
1614
1615        let tick_driver = node.spin();
1616
1617        let counts = into_singleton
1618            .snapshot(&node_tick, nondet!(/** test */))
1619            .into_stream()
1620            .count()
1621            .zip(tick_driver.batch(&node_tick, nondet!(/** test */)).count())
1622            .map(q!(|(c, _)| c))
1623            .all_ticks()
1624            .send_bincode_external(&external);
1625
1626        let nodes = flow
1627            .with_process(&node, deployment.Localhost())
1628            .with_external(&external, deployment.Localhost())
1629            .deploy(&mut deployment);
1630
1631        deployment.deploy().await.unwrap();
1632
1633        let mut external_out = nodes.connect(counts).await;
1634
1635        deployment.start().await.unwrap();
1636
1637        assert_eq!(external_out.next().await.unwrap(), 1);
1638        assert_eq!(external_out.next().await.unwrap(), 1);
1639        assert_eq!(external_out.next().await.unwrap(), 1);
1640    }
1641
1642    #[cfg(feature = "deploy")]
1643    #[tokio::test]
1644    async fn into_singleton_unbounded_top_level_none_cardinality() {
1645        let mut deployment = Deployment::new();
1646
1647        let mut flow = FlowBuilder::new();
1648        let node = flow.process::<()>();
1649        let external = flow.external::<()>();
1650
1651        let top_level_none = node
1652            .tick()
1653            .singleton(q!(123))
1654            .latest()
1655            .filter(q!(|_| false));
1656        let into_singleton = top_level_none.into_singleton();
1657
1658        let tick_driver = node.spin();
1659
1660        let tick_later = node.tick();
1661        let counts = into_singleton
1662            .snapshot(&tick_later, nondet!(/** test */))
1663            .into_stream()
1664            .count()
1665            .zip(tick_driver.batch(&tick_later, nondet!(/** test */)).count())
1666            .map(q!(|(c, _)| c))
1667            .all_ticks()
1668            .send_bincode_external(&external);
1669
1670        let nodes = flow
1671            .with_process(&node, deployment.Localhost())
1672            .with_external(&external, deployment.Localhost())
1673            .deploy(&mut deployment);
1674
1675        deployment.deploy().await.unwrap();
1676
1677        let mut external_out = nodes.connect(counts).await;
1678
1679        deployment.start().await.unwrap();
1680
1681        assert_eq!(external_out.next().await.unwrap(), 1);
1682        assert_eq!(external_out.next().await.unwrap(), 1);
1683        assert_eq!(external_out.next().await.unwrap(), 1);
1684    }
1685
1686    #[cfg(feature = "sim")]
1687    #[test]
1688    fn top_level_optional_some_into_stream_no_replay() {
1689        let mut flow = FlowBuilder::new();
1690        let node = flow.process::<()>();
1691
1692        let source_iter = node.source_iter(q!(vec![1, 2, 3, 4]));
1693        let folded = source_iter.fold(q!(|| 0), q!(|a, b| *a += b));
1694        let filtered_some = folded.filter(q!(|_| true));
1695
1696        let out_recv = filtered_some.into_stream().sim_output();
1697
1698        flow.sim().exhaustive(async || {
1699            out_recv.assert_yields_only([10]).await;
1700        });
1701    }
1702
1703    #[cfg(feature = "sim")]
1704    #[test]
1705    fn top_level_optional_none_into_stream_no_replay() {
1706        let mut flow = FlowBuilder::new();
1707        let node = flow.process::<()>();
1708
1709        let source_iter = node.source_iter(q!(vec![1, 2, 3, 4]));
1710        let folded = source_iter.fold(q!(|| 0), q!(|a, b| *a += b));
1711        let filtered_none = folded.filter(q!(|_| false));
1712
1713        let out_recv = filtered_none.into_stream().sim_output();
1714
1715        flow.sim().exhaustive(async || {
1716            out_recv.assert_yields_only([] as [i32; 0]).await;
1717        });
1718    }
1719
1720    #[cfg(feature = "deploy")]
1721    #[tokio::test]
1722    async fn test_optional_ref() {
1723        let mut deployment = Deployment::new();
1724
1725        let mut flow = FlowBuilder::new();
1726        let external = flow.external::<()>();
1727        let p1 = flow.process::<()>();
1728
1729        // Create an optional: reduce 0..5 => Some(10) (sum via reduce)
1730        let my_opt = p1.source_iter(q!(0..5i32)).reduce(q!(|a, b| *a += b));
1731
1732        let opt_ref = my_opt.by_ref();
1733
1734        // Use the optional ref in a map: unwrap_or(0) + x
1735        let out_port = p1
1736            .source_iter(q!(1..=3i32))
1737            .map(q!(|x| x + opt_ref.unwrap_or(0)))
1738            .send_bincode_external(&external);
1739
1740        let nodes = flow
1741            .with_default_optimize()
1742            .with_process(&p1, deployment.Localhost())
1743            .with_external(&external, deployment.Localhost())
1744            .deploy(&mut deployment);
1745
1746        deployment.deploy().await.unwrap();
1747
1748        let mut out_recv = nodes.connect(out_port).await;
1749
1750        deployment.start().await.unwrap();
1751
1752        let mut results = Vec::new();
1753        for _ in 0..3 {
1754            results.push(out_recv.next().await.unwrap());
1755        }
1756        results.sort();
1757        // reduce(0..5) = 10, so results should be 11, 12, 13
1758        assert_eq!(results, vec![11, 12, 13]);
1759    }
1760
1761    #[cfg(feature = "deploy")]
1762    #[tokio::test]
1763    async fn test_optional_ref_none() {
1764        let mut deployment = Deployment::new();
1765
1766        let mut flow = FlowBuilder::new();
1767        let external = flow.external::<()>();
1768        let p1 = flow.process::<()>();
1769
1770        // Create an optional from an empty source => None
1771        let my_opt = p1
1772            .source_iter(q!(std::iter::empty::<i32>()))
1773            .reduce(q!(|a, b| *a += b));
1774
1775        let opt_ref = my_opt.by_ref();
1776
1777        // Use the optional ref: should be None, so unwrap_or(99)
1778        let out_port = p1
1779            .source_iter(q!(1..=2i32))
1780            .map(q!(|x| x + opt_ref.unwrap_or(99)))
1781            .send_bincode_external(&external);
1782
1783        let nodes = flow
1784            .with_default_optimize()
1785            .with_process(&p1, deployment.Localhost())
1786            .with_external(&external, deployment.Localhost())
1787            .deploy(&mut deployment);
1788
1789        deployment.deploy().await.unwrap();
1790
1791        let mut out_recv = nodes.connect(out_port).await;
1792
1793        deployment.start().await.unwrap();
1794
1795        let mut results = Vec::new();
1796        for _ in 0..2 {
1797            results.push(out_recv.next().await.unwrap());
1798        }
1799        results.sort();
1800        // optional is None, so unwrap_or(99) => 100, 101
1801        assert_eq!(results, vec![100, 101]);
1802    }
1803
1804    #[cfg(feature = "deploy")]
1805    #[tokio::test]
1806    async fn test_optional_ref_and_consume() {
1807        let mut deployment = Deployment::new();
1808
1809        let mut flow = FlowBuilder::new();
1810        let external = flow.external::<()>();
1811        let p1 = flow.process::<()>();
1812
1813        // Use reduce to produce an Optional
1814        let my_opt = p1.source_iter(q!(0..5i32)).reduce(q!(|a, b| *a += b));
1815
1816        let opt_ref = my_opt.by_ref();
1817
1818        // Reference path
1819        let out_port_ref = p1
1820            .source_iter(q!(1..=2i32))
1821            .map(q!(|x| x + opt_ref.unwrap_or(0)))
1822            .send_bincode_external(&external);
1823
1824        let nodes = flow
1825            .with_default_optimize()
1826            .with_process(&p1, deployment.Localhost())
1827            .with_external(&external, deployment.Localhost())
1828            .deploy(&mut deployment);
1829
1830        deployment.deploy().await.unwrap();
1831
1832        let mut out_recv_ref = nodes.connect(out_port_ref).await;
1833
1834        deployment.start().await.unwrap();
1835
1836        let mut ref_results = Vec::new();
1837        for _ in 0..2 {
1838            ref_results.push(out_recv_ref.next().await.unwrap());
1839        }
1840        ref_results.sort();
1841        // reduce(0..5) = 10, so 1+10=11, 2+10=12
1842        assert_eq!(ref_results, vec![11, 12]);
1843    }
1844}