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