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