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