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