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