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