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