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>,
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>,
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>>>,
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>>>,
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>>>,
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 pub fn sample_every(
1338 self,
1339 interval: impl QuotedWithContext<'a, std::time::Duration, L> + Copy + 'a,
1340 mut nondet: NonDet<(
1341 Option<crate::sim_hooks::SnapshotHook<T>>,
1342 Option<crate::sim_hooks::BatchHook<()>>,
1343 )>,
1344 ) -> Stream<T, L::DropConsistency, Unbounded, TotalOrder, AtLeastOnce>
1345 where
1346 L: TopLevel<'a>,
1347 {
1348 let samples = self.location.source_interval(interval);
1349 let (snapshot_hook, samples_hook) = nondet.take_hook();
1350 sliced! {
1351 let snapshot = use::snapshot(self, nondet!(
1352 /// which snapshots are sampled is captured by the caller's guard
1353 hook = snapshot_hook
1354 ));
1355 let sample_batch = use::batch(samples, nondet!(
1356 /// sample timing is captured by the caller's guard
1357 hook = samples_hook
1358 ));
1359
1360 snapshot.filter_if(sample_batch.first().is_some()).into_stream()
1361 }
1362 .weaken_retries()
1363 }
1364
1365 /// Strengthens the boundedness guarantee to `Bounded`, given that `B: IsBounded`, which
1366 /// implies that `B == Bounded`.
1367 pub fn make_bounded(self) -> Singleton<T, L, Bounded>
1368 where
1369 B: IsBounded,
1370 {
1371 Singleton::new(
1372 self.location.clone(),
1373 self.ir_node.replace(HydroNode::Placeholder),
1374 )
1375 }
1376
1377 fn try_make_bounded(self) -> Result<Singleton<T, L, Bounded>, Singleton<T, L, B>> {
1378 if B::UnderlyingBound::BOUNDED {
1379 Ok(Singleton::new(
1380 self.location.clone(),
1381 self.ir_node.replace(HydroNode::Placeholder),
1382 ))
1383 } else {
1384 Err(self)
1385 }
1386 }
1387
1388 /// Clones this bounded singleton into a tick, returning a singleton that has the
1389 /// same value as the outer singleton. Because the outer singleton is bounded, this
1390 /// is deterministic because there is only a single immutable version.
1391 pub fn clone_into_tick<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1392 self,
1393 tick: &Tick<L2>,
1394 ) -> Singleton<T, Tick<L2>, Bounded>
1395 where
1396 B: IsBounded,
1397 T: Clone,
1398 {
1399 // TODO(shadaj): avoid printing simulator logs for this snapshot
1400 let inner = self.snapshot(
1401 tick,
1402 nondet!(/** bounded top-level singleton so deterministic */),
1403 );
1404 Singleton::new(tick.clone(), inner.ir_node.replace(HydroNode::Placeholder))
1405 }
1406
1407 /// Converts this singleton into a [`Stream`] containing a single element, the value.
1408 ///
1409 /// # Example
1410 /// ```rust
1411 /// # #[cfg(feature = "deploy")] {
1412 /// # use hydro_lang::prelude::*;
1413 /// # use futures::StreamExt;
1414 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1415 /// let tick = process.tick();
1416 /// let batch_input = process
1417 /// .source_iter(q!(vec![123, 456]))
1418 /// .batch(&tick, nondet!(/** test */));
1419 /// batch_input.clone().chain(
1420 /// batch_input.count().into_stream()
1421 /// ).all_ticks()
1422 /// # }, |mut stream| async move {
1423 /// // [123, 456, 2]
1424 /// # for w in vec![123, 456, 2] {
1425 /// # assert_eq!(stream.next().await.unwrap(), w);
1426 /// # }
1427 /// # }));
1428 /// # }
1429 /// ```
1430 pub fn into_stream(self) -> Stream<T, L, Bounded, TotalOrder, ExactlyOnce>
1431 where
1432 B: IsBounded,
1433 {
1434 Stream::new(
1435 self.location.clone(),
1436 HydroNode::Cast {
1437 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1438 metadata: self.location.new_node_metadata(Stream::<
1439 T,
1440 Tick<L>,
1441 Bounded,
1442 TotalOrder,
1443 ExactlyOnce,
1444 >::collection_kind()),
1445 },
1446 )
1447 }
1448
1449 /// Resolves the singleton's [`Future`] value by blocking until it completes,
1450 /// producing a singleton of the resolved output.
1451 ///
1452 /// This is useful when the singleton contains an async computation that must
1453 /// be awaited before further processing. The future is polled to completion
1454 /// before the output value is emitted.
1455 ///
1456 /// # Example
1457 /// ```rust
1458 /// # #[cfg(feature = "deploy")] {
1459 /// # use hydro_lang::prelude::*;
1460 /// # use futures::StreamExt;
1461 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1462 /// let tick = process.tick();
1463 /// let singleton = tick.singleton(q!(5));
1464 /// singleton
1465 /// .map(q!(|v| async move { v * 2 }))
1466 /// .resolve_future_blocking()
1467 /// .all_ticks()
1468 /// # }, |mut stream| async move {
1469 /// // 10
1470 /// # assert_eq!(stream.next().await.unwrap(), 10);
1471 /// # }));
1472 /// # }
1473 /// ```
1474 pub fn resolve_future_blocking(
1475 self,
1476 ) -> Singleton<T::Output, L, <B as SingletonBound>::UnderlyingBound>
1477 where
1478 T: Future,
1479 B: IsBounded,
1480 {
1481 Singleton::new(
1482 self.location.clone(),
1483 HydroNode::ResolveFuturesBlocking {
1484 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1485 metadata: self
1486 .location
1487 .new_node_metadata(Singleton::<T::Output, L, B>::collection_kind()),
1488 },
1489 )
1490 }
1491}
1492
1493impl<'a, T, L> Singleton<T, Tick<L>, Bounded>
1494where
1495 L: Location<'a>,
1496{
1497 /// Asynchronously yields the value of this singleton outside the tick as an unbounded stream,
1498 /// which will stream the value computed in _each_ tick as a separate stream element.
1499 ///
1500 /// Unlike [`Singleton::latest`], the value computed in each tick is emitted separately,
1501 /// producing one element in the output for each tick. This is useful for batched computations,
1502 /// where the results from each tick must be combined together.
1503 ///
1504 /// # Example
1505 /// ```rust
1506 /// # #[cfg(feature = "deploy")] {
1507 /// # use hydro_lang::prelude::*;
1508 /// # use futures::StreamExt;
1509 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1510 /// let tick = process.tick();
1511 /// # // ticks are lazy by default, forces the second tick to run
1512 /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1513 /// # let batch_first_tick = process
1514 /// # .source_iter(q!(vec![1]))
1515 /// # .batch(&tick, nondet!(/** test */));
1516 /// # let batch_second_tick = process
1517 /// # .source_iter(q!(vec![1, 2, 3]))
1518 /// # .batch(&tick, nondet!(/** test */))
1519 /// # .defer_tick(); // appears on the second tick
1520 /// # let input_batch = batch_first_tick.chain(batch_second_tick);
1521 /// input_batch // first tick: [1], second tick: [1, 2, 3]
1522 /// .count()
1523 /// .all_ticks()
1524 /// # }, |mut stream| async move {
1525 /// // [1, 3]
1526 /// # for w in vec![1, 3] {
1527 /// # assert_eq!(stream.next().await.unwrap(), w);
1528 /// # }
1529 /// # }));
1530 /// # }
1531 /// ```
1532 pub fn all_ticks(self) -> Stream<T, L, Unbounded, TotalOrder, ExactlyOnce> {
1533 self.into_stream().all_ticks()
1534 }
1535
1536 /// Synchronously yields the value of this singleton outside the tick as an unbounded stream,
1537 /// which will stream the value computed in _each_ tick as a separate stream element.
1538 ///
1539 /// Unlike [`Singleton::all_ticks`], this preserves synchronous execution, as the output stream
1540 /// is emitted in an [`Atomic`] context that will process elements synchronously with the input
1541 /// singleton's [`Tick`] context.
1542 pub fn all_ticks_atomic(self) -> Stream<T, Atomic<L>, Unbounded, TotalOrder, ExactlyOnce> {
1543 self.into_stream().all_ticks_atomic()
1544 }
1545
1546 /// Asynchronously yields this singleton outside the tick as an unbounded [`Optional`], which
1547 /// will be asynchronously updated with the latest value of the singleton inside the tick.
1548 ///
1549 /// The result is an [`Optional`] rather than a [`Singleton`] because the producing tick does
1550 /// not have to have run yet: before its first run there is no value, so the optional is null.
1551 /// Once the tick has run the optional becomes non-null and stays non-null (its value tracks
1552 /// the latest tick), hence the [`InitNone`] boundedness.
1553 ///
1554 /// This converts a bounded value _inside_ a tick into an asynchronous value outside the
1555 /// tick that tracks the inner value. This is useful for getting the value as of the
1556 /// "most recent" tick, but note that updates are propagated asynchronously outside the tick.
1557 ///
1558 /// # Example
1559 /// ```rust
1560 /// # #[cfg(feature = "deploy")] {
1561 /// # use hydro_lang::prelude::*;
1562 /// # use futures::StreamExt;
1563 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1564 /// let tick = process.tick();
1565 /// # // ticks are lazy by default, forces the second tick to run
1566 /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1567 /// # let batch_first_tick = process
1568 /// # .source_iter(q!(vec![1]))
1569 /// # .batch(&tick, nondet!(/** test */));
1570 /// # let batch_second_tick = process
1571 /// # .source_iter(q!(vec![1, 2, 3]))
1572 /// # .batch(&tick, nondet!(/** test */))
1573 /// # .defer_tick(); // appears on the second tick
1574 /// # let input_batch = batch_first_tick.chain(batch_second_tick);
1575 /// input_batch // first tick: [1], second tick: [1, 2, 3]
1576 /// .count()
1577 /// .latest()
1578 /// .unwrap_or(process.singleton(q!(0usize)).into())
1579 /// # .sample_eager(nondet!(/** test */))
1580 /// # }, |mut stream| async move {
1581 /// // asynchronously changes from 1 ~> 3
1582 /// # for w in vec![1, 3] {
1583 /// # assert_eq!(stream.next().await.unwrap(), w);
1584 /// # }
1585 /// # }));
1586 /// # }
1587 /// ```
1588 pub fn latest(self) -> Optional<T, L, InitNone> {
1589 Optional::new(
1590 self.location.parent_location().clone(),
1591 HydroNode::YieldConcat {
1592 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1593 metadata: self
1594 .location
1595 .parent_location()
1596 .new_node_metadata(Optional::<T, L, InitNone>::collection_kind()),
1597 },
1598 )
1599 }
1600
1601 /// Synchronously yields this singleton outside the tick as an unbounded [`Optional`], which
1602 /// will be updated with the latest value of the singleton inside the tick.
1603 ///
1604 /// Unlike [`Singleton::latest`], this preserves synchronous execution, as the output optional
1605 /// is emitted in an [`Atomic`] context that will process elements synchronously with the input
1606 /// singleton's [`Tick`] context. As with [`Singleton::latest`], the result is an [`Optional`]
1607 /// ([`InitNone`]) because it is null until the producing tick first runs.
1608 pub fn latest_atomic(self) -> Optional<T, Atomic<L>, InitNone> {
1609 let out_location = Atomic {
1610 tick: self.location.clone(),
1611 };
1612 Optional::new(
1613 out_location.clone(),
1614 HydroNode::YieldConcat {
1615 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1616 metadata: out_location
1617 .new_node_metadata(Optional::<T, Atomic<L>, InitNone>::collection_kind()),
1618 },
1619 )
1620 }
1621}
1622
1623#[doc(hidden)]
1624/// Helper trait that determines the output collection type for [`Singleton::zip`].
1625///
1626/// The output will be an [`Optional`] if the second input is an [`Optional`], otherwise it is a
1627/// [`Singleton`].
1628#[sealed::sealed]
1629pub trait ZipResult<'a, Other> {
1630 /// The output collection type.
1631 type Out;
1632 /// The type of the tupled output value.
1633 type ElementType;
1634 /// The type of the other collection's value.
1635 type OtherType;
1636 /// The location where the tupled result will be materialized.
1637 type Location: Location<'a>;
1638
1639 /// The location of the second input to the `zip`.
1640 fn other_location(other: &Other) -> Self::Location;
1641 /// The IR node of the second input to the `zip`.
1642 fn other_ir_node(other: Other) -> HydroNode;
1643
1644 /// Constructs the output live collection given an IR node containing the zip result.
1645 fn make(location: Self::Location, ir_node: HydroNode) -> Self::Out;
1646}
1647
1648#[sealed::sealed]
1649impl<'a, T, U, L, B: SingletonBound> ZipResult<'a, Singleton<U, L, B>> for Singleton<T, L, B>
1650where
1651 L: Location<'a>,
1652{
1653 type Out = Singleton<(T, U), L, B>;
1654 type ElementType = (T, U);
1655 type OtherType = U;
1656 type Location = L;
1657
1658 fn other_location(other: &Singleton<U, L, B>) -> L {
1659 other.location.clone()
1660 }
1661
1662 fn other_ir_node(other: Singleton<U, L, B>) -> HydroNode {
1663 other.ir_node.replace(HydroNode::Placeholder)
1664 }
1665
1666 fn make(location: L, ir_node: HydroNode) -> Self::Out {
1667 Singleton::new(
1668 location.clone(),
1669 HydroNode::Cast {
1670 inner: Box::new(ir_node),
1671 metadata: location.new_node_metadata(Self::Out::collection_kind()),
1672 },
1673 )
1674 }
1675}
1676
1677#[sealed::sealed]
1678impl<'a, T, U, L, B: SingletonBound> ZipResult<'a, Optional<U, L, B::UnderlyingBound>>
1679 for Singleton<T, L, B>
1680where
1681 L: Location<'a>,
1682{
1683 type Out = Optional<(T, U), L, B::UnderlyingBound>;
1684 type ElementType = (T, U);
1685 type OtherType = U;
1686 type Location = L;
1687
1688 fn other_location(other: &Optional<U, L, B::UnderlyingBound>) -> L {
1689 other.location.clone()
1690 }
1691
1692 fn other_ir_node(other: Optional<U, L, B::UnderlyingBound>) -> HydroNode {
1693 other.ir_node.replace(HydroNode::Placeholder)
1694 }
1695
1696 fn make(location: L, ir_node: HydroNode) -> Self::Out {
1697 Optional::new(location, ir_node)
1698 }
1699}
1700
1701#[cfg(test)]
1702mod tests {
1703 #[cfg(feature = "deploy")]
1704 use futures::{SinkExt, StreamExt};
1705 #[cfg(feature = "deploy")]
1706 use hydro_deploy::Deployment;
1707 #[cfg(any(feature = "deploy", feature = "sim"))]
1708 use stageleft::q;
1709
1710 #[cfg(any(feature = "deploy", feature = "sim"))]
1711 use crate::compile::builder::FlowBuilder;
1712 #[cfg(feature = "deploy")]
1713 use crate::live_collections::stream::ExactlyOnce;
1714 #[cfg(any(feature = "deploy", feature = "sim"))]
1715 use crate::location::Location;
1716 #[cfg(any(feature = "deploy", feature = "sim"))]
1717 use crate::nondet::nondet;
1718
1719 #[cfg(feature = "deploy")]
1720 #[tokio::test]
1721 async fn tick_cycle_cardinality() {
1722 let mut deployment = Deployment::new();
1723
1724 let mut flow = FlowBuilder::new();
1725 let node = flow.process::<()>();
1726 let external = flow.external::<()>();
1727
1728 let (input_send, input) = node.source_external_bincode::<_, _, _, ExactlyOnce>(&external);
1729
1730 let node_tick = node.tick();
1731 let (complete_cycle, singleton) = node_tick.cycle_with_initial(node_tick.singleton(q!(0)));
1732 let counts = singleton
1733 .clone()
1734 .into_stream()
1735 .count()
1736 .filter_if(
1737 input
1738 .batch(&node_tick, nondet!(/** testing */))
1739 .first()
1740 .is_some(),
1741 )
1742 .all_ticks()
1743 .send_bincode_external(&external);
1744 complete_cycle.complete_next_tick(singleton);
1745
1746 let nodes = flow
1747 .with_process(&node, deployment.Localhost())
1748 .with_external(&external, deployment.Localhost())
1749 .deploy(&mut deployment);
1750
1751 deployment.deploy().await.unwrap();
1752
1753 let mut tick_trigger = nodes.connect(input_send).await;
1754 let mut external_out = nodes.connect(counts).await;
1755
1756 deployment.start().await.unwrap();
1757
1758 tick_trigger.send(()).await.unwrap();
1759
1760 assert_eq!(external_out.next().await.unwrap(), 1);
1761
1762 tick_trigger.send(()).await.unwrap();
1763
1764 assert_eq!(external_out.next().await.unwrap(), 1);
1765 }
1766
1767 #[cfg(feature = "sim")]
1768 #[test]
1769 #[should_panic]
1770 fn sim_fold_intermediate_states() {
1771 let mut flow = FlowBuilder::new();
1772 let node = flow.process::<()>();
1773
1774 let source = node.source_stream(q!(tokio_stream::iter(vec![1, 2, 3, 4])));
1775 let folded = source.fold(q!(|| 0), q!(|a, b| *a += b));
1776
1777 let tick = node.tick();
1778 let batch = folded.snapshot(&tick, nondet!(/** test */));
1779 let out_recv = batch.all_ticks().sim_output();
1780
1781 flow.sim().exhaustive(async || {
1782 assert_eq!(out_recv.next().await, 10);
1783 });
1784 }
1785
1786 #[cfg(feature = "sim")]
1787 #[test]
1788 fn sim_fold_intermediate_state_count() {
1789 let mut flow = FlowBuilder::new();
1790 let node = flow.process::<()>();
1791
1792 let source = node.source_stream(q!(tokio_stream::iter(vec![1, 2, 3, 4])));
1793 let folded = source.fold(q!(|| 0), q!(|a, b| *a += b));
1794
1795 let tick = node.tick();
1796 let batch = folded.snapshot(&tick, nondet!(/** test */));
1797 let out_recv = batch.all_ticks().sim_output();
1798
1799 let instance_count = flow.sim().exhaustive(async || {
1800 let out = out_recv.collect::<Vec<_>>().await;
1801 assert_eq!(out.last(), Some(&10));
1802 });
1803
1804 assert_eq!(
1805 instance_count,
1806 16 // 2^4 possible subsets of intermediates (including initial state)
1807 )
1808 }
1809
1810 #[cfg(feature = "sim")]
1811 #[test]
1812 fn sim_fold_no_repeat_initial() {
1813 // check that we don't repeat the initial state of the fold in autonomous decisions
1814
1815 let mut flow = FlowBuilder::new();
1816 let node = flow.process::<()>();
1817
1818 let (in_port, input) = node.sim_input();
1819 let folded = input.fold(q!(|| 0), q!(|a, b| *a += b));
1820
1821 let tick = node.tick();
1822 let batch = folded.snapshot(&tick, nondet!(/** test */));
1823 let out_recv = batch.all_ticks().sim_output();
1824
1825 flow.sim().exhaustive(async || {
1826 assert_eq!(out_recv.next().await, 0);
1827
1828 in_port.send(123);
1829
1830 assert_eq!(out_recv.next().await, 123);
1831 });
1832 }
1833
1834 #[cfg(feature = "sim")]
1835 #[test]
1836 #[should_panic]
1837 fn sim_fold_repeats_snapshots() {
1838 // when the tick is driven by a snapshot AND something else, the snapshot can
1839 // "stutter" and repeat the same state multiple times
1840
1841 let mut flow = FlowBuilder::new();
1842 let node = flow.process::<()>();
1843
1844 let source = node.source_stream(q!(tokio_stream::iter(vec![1, 2, 3, 4])));
1845 let folded = source.clone().fold(q!(|| 0), q!(|a, b| *a += b));
1846
1847 let tick = node.tick();
1848 let batch = source
1849 .batch(&tick, nondet!(/** test */))
1850 .cross_singleton(folded.snapshot(&tick, nondet!(/** test */)));
1851 let out_recv = batch.all_ticks().sim_output();
1852
1853 flow.sim().exhaustive(async || {
1854 if out_recv.next().await == (1, 3) && out_recv.next().await == (2, 3) {
1855 panic!("repeated snapshot");
1856 }
1857 });
1858 }
1859
1860 #[cfg(feature = "sim")]
1861 #[test]
1862 fn sim_fold_repeats_snapshots_count() {
1863 // check the number of instances
1864 let mut flow = FlowBuilder::new();
1865 let node = flow.process::<()>();
1866
1867 let source = node.source_stream(q!(tokio_stream::iter(vec![1, 2])));
1868 let folded = source.clone().fold(q!(|| 0), q!(|a, b| *a += b));
1869
1870 let tick = node.tick();
1871 let batch = source
1872 .batch(&tick, nondet!(/** test */))
1873 .cross_singleton(folded.snapshot(&tick, nondet!(/** test */)));
1874 let out_recv = batch.all_ticks().sim_output();
1875
1876 let count = flow.sim().exhaustive(async || {
1877 let _ = out_recv.collect::<Vec<_>>().await;
1878 });
1879
1880 assert_eq!(count, 52);
1881 }
1882
1883 #[cfg(feature = "sim")]
1884 #[test]
1885 fn sim_top_level_singleton_exhaustive() {
1886 // ensures that top-level singletons have only one snapshot
1887 let mut flow = FlowBuilder::new();
1888 let node = flow.process::<()>();
1889
1890 let singleton = node.singleton(q!(1));
1891 let tick = node.tick();
1892 let batch = singleton.snapshot(&tick, nondet!(/** test */));
1893 let out_recv = batch.all_ticks().sim_output();
1894
1895 let count = flow.sim().exhaustive(async || {
1896 let _ = out_recv.collect::<Vec<_>>().await;
1897 });
1898
1899 assert_eq!(count, 1);
1900 }
1901
1902 #[cfg(feature = "sim")]
1903 #[test]
1904 fn sim_top_level_singleton_join_count() {
1905 // if a tick consumes a static snapshot and a stream batch, only the batch require space
1906 // exploration
1907
1908 let mut flow = FlowBuilder::new();
1909 let node = flow.process::<()>();
1910
1911 let source_iter = node.source_iter(q!(vec![1, 2, 3, 4]));
1912 let tick = node.tick();
1913 let batch = source_iter
1914 .batch(&tick, nondet!(/** test */))
1915 .cross_singleton(node.singleton(q!(123)).clone_into_tick(&tick));
1916 let out_recv = batch.all_ticks().sim_output();
1917
1918 let instance_count = flow.sim().exhaustive(async || {
1919 let _ = out_recv.collect::<Vec<_>>().await;
1920 });
1921
1922 assert_eq!(
1923 instance_count,
1924 16 // 2^4 ways to split up (including a possibly empty first batch)
1925 )
1926 }
1927
1928 #[cfg(feature = "sim")]
1929 #[test]
1930 fn top_level_singleton_into_stream_no_replay() {
1931 let mut flow = FlowBuilder::new();
1932 let node = flow.process::<()>();
1933
1934 let source_iter = node.source_iter(q!(vec![1, 2, 3, 4]));
1935 let folded = source_iter.fold(q!(|| 0), q!(|a, b| *a += b));
1936
1937 let out_recv = folded.into_stream().sim_output();
1938
1939 flow.sim().exhaustive(async || {
1940 out_recv.assert_yields_only([10]).await;
1941 });
1942 }
1943
1944 #[cfg(feature = "sim")]
1945 #[test]
1946 fn inside_tick_singleton_zip() {
1947 use crate::live_collections::Stream;
1948 use crate::live_collections::sliced::sliced;
1949
1950 let mut flow = FlowBuilder::new();
1951 let node = flow.process::<()>();
1952
1953 let source_iter: Stream<_, _> = node.source_iter(q!(vec![1, 2])).into();
1954 let folded = source_iter.fold(q!(|| 0), q!(|a, b| *a += b));
1955
1956 let out_recv = sliced! {
1957 let v = use::snapshot(folded, nondet!(/** test */));
1958 v.clone().zip(v).into_stream()
1959 }
1960 .sim_output();
1961
1962 let count = flow.sim().exhaustive(async || {
1963 let out = out_recv.collect::<Vec<_>>().await;
1964 assert_eq!(out.last(), Some(&(3, 3)));
1965 });
1966
1967 assert_eq!(count, 4);
1968 }
1969
1970 /// Reproducer for simulator hang when using cross_singleton on a top-level
1971 /// unbounded stream (not inside sliced!). The exhaustive simulator hangs
1972 /// after the first iteration.
1973 #[cfg(feature = "sim")]
1974 #[test]
1975 fn sim_cross_singleton_top_level_unbounded_hang() {
1976 let mut flow = FlowBuilder::new();
1977 let node = flow.process::<()>();
1978
1979 let (cmd_port, input) = node.sim_input::<String, _, _>();
1980
1981 let top_level_singleton = node.singleton(q!(123));
1982
1983 // cross_singleton on a top-level stream - bug trigger
1984 let crossed = input.cross_singleton(top_level_singleton);
1985
1986 // Output directly
1987 let resp_port = crossed.sim_output();
1988
1989 let count = flow.sim().exhaustive(async || {
1990 cmd_port.send("abc".to_owned());
1991
1992 let responses: Vec<_> = resp_port.collect().await;
1993 assert!(!responses.is_empty());
1994 });
1995
1996 assert_eq!(count, 1);
1997 }
1998
1999 #[cfg(feature = "sim")]
2000 #[test]
2001 fn sim_top_level_singleton_state_count() {
2002 let mut flow = FlowBuilder::new();
2003 let process = flow.process::<()>();
2004
2005 let (cmd_port, input) = process.sim_input();
2006 {
2007 // increases exhaustive inputs from 1 to 2 before we optimized `From`
2008 use super::Singleton;
2009 use crate::live_collections::boundedness::Unbounded;
2010 let _singleton: Singleton<_, _, Unbounded> = process.singleton(q!(false)).into();
2011 }
2012 let tick = process.tick();
2013 let batched_unbatched = input.batch(&tick, nondet!(/** */)).all_ticks();
2014 let resp_port = batched_unbatched.sim_output();
2015
2016 let count = flow.sim().exhaustive(async || {
2017 cmd_port.send(());
2018 let _responses: Vec<_> = resp_port.collect().await;
2019 });
2020
2021 assert_eq!(count, 1);
2022 }
2023
2024 /// Regression test for #2939: singleton mut access-group counter resets per root.
2025 /// Two sequential `by_mut` captures on the same singleton, consumed by separate
2026 /// `for_each` roots, should get distinct access groups and build successfully.
2027 #[cfg(feature = "sim")]
2028 #[test]
2029 #[expect(unused_mut, reason = "sliced! macro generates mut bindings for state")]
2030 fn sim_mut_access_group_across_roots() {
2031 use crate::live_collections::sliced::sliced;
2032
2033 let mut flow = FlowBuilder::new();
2034 let node = flow.process::<()>();
2035
2036 let source = node.source_iter(q!(vec![1i32, 2, 3]));
2037
2038 let (first, second) = sliced! {
2039 let batch = use::batch(source, nondet!(/** test */));
2040 let mut total = use::state(|l| l.singleton(q!(0i32)));
2041 let total_mut = total.by_mut();
2042
2043 let first = batch.clone().map(q!(|x| {
2044 *total_mut += x;
2045 *total_mut
2046 }));
2047 let second = batch.map(q!(|x| {
2048 *total_mut += x;
2049 *total_mut
2050 }));
2051 (first, second)
2052 };
2053
2054 let first_recv = first.sim_output();
2055 let second_recv = second.sim_output();
2056
2057 flow.sim().exhaustive(async || {
2058 // Both outputs should produce values without panicking.
2059 // The exact values depend on ordering, but the graph must build.
2060 let _first: Vec<i32> = first_recv.collect().await;
2061 let _second: Vec<i32> = second_recv.collect().await;
2062 });
2063 }
2064
2065 /// Regression test for #2940: access groups must follow code (staging) order,
2066 /// not IR traversal order. When `second.chain(first)` reverses the consumption
2067 /// order, the mutations must still execute in the order they were staged.
2068 #[cfg(feature = "sim")]
2069 #[test]
2070 #[expect(unused_mut, reason = "sliced! macro generates mut bindings for state")]
2071 fn sim_mut_access_groups_follow_code_order() {
2072 use crate::live_collections::sliced::sliced;
2073
2074 let mut flow = FlowBuilder::new();
2075 let node = flow.process::<()>();
2076
2077 let source = node.source_iter(q!(vec![3i32]));
2078
2079 let out_recv = sliced! {
2080 let batch = use::batch(source, nondet!(/** test */));
2081 let mut total = use::state(|l| l.singleton(q!(0i32)));
2082 let total_mut = total.by_mut();
2083
2084 // Defined FIRST in code: addition
2085 let first = batch.clone().map(q!(|x| {
2086 *total_mut += x;
2087 *total_mut
2088 }));
2089 // Defined SECOND in code: doubling
2090 let second = batch.map(q!(|_x| {
2091 *total_mut *= 2;
2092 *total_mut
2093 }));
2094 // Chain in OPPOSITE order of definition — must not affect mutation order.
2095 second.chain(first)
2096 }
2097 .sim_output();
2098
2099 flow.sim().exhaustive(async || {
2100 let results: Vec<i32> = out_recv.collect().await;
2101 // Code-order semantics: first runs (total = 0 + 3 = 3), then second
2102 // runs (total = 3 * 2 = 6). Output is second.chain(first) => [6, 3].
2103 assert_eq!(results, vec![6, 3]);
2104 });
2105 }
2106}