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