Skip to main content

hydro_lang/live_collections/
singleton.rs

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