hydro_lang/live_collections/stream/mod.rs
1//! Definitions for the [`Stream`] live collection.
2
3use std::cell::RefCell;
4use std::future::Future;
5use std::hash::Hash;
6use std::marker::PhantomData;
7use std::ops::Deref;
8use std::rc::Rc;
9
10use stageleft::{IntoQuotedMut, QuotedWithContext, QuotedWithContextWithProps, q, quote_type};
11
12use super::OperatorContext;
13use super::boundedness::{Bounded, Boundedness, IsBounded, Unbounded};
14use super::keyed_singleton::KeyedSingleton;
15use super::keyed_stream::{Generate, KeyedStream};
16use super::optional::Optional;
17use super::singleton::Singleton;
18#[cfg(feature = "tokio")]
19use super::sliced::sliced;
20use crate::compile::builder::{CycleId, FlowState};
21use crate::compile::ir::{
22 CollectionKind, HydroIrOpMetadata, HydroNode, HydroRoot, SharedNode, StreamOrder, StreamRetry,
23};
24#[cfg(stageleft_runtime)]
25use crate::forward_handle::{CycleCollection, CycleCollectionWithInitial, ReceiverComplete};
26use crate::forward_handle::{ForwardRef, TickCycle};
27use crate::live_collections::batch_atomic::BatchAtomic;
28use crate::live_collections::singleton::SingletonBound;
29#[cfg(stageleft_runtime)]
30use crate::location::dynamic::{DynLocation, LocationId};
31use crate::location::tick::{Atomic, DeferTick};
32use crate::location::{Location, Tick, TopLevel, check_matching_location};
33use crate::manual_expr::ManualExpr;
34use crate::nondet::{NonDet, nondet};
35use crate::prelude::manual_proof;
36use crate::properties::{
37 AggFuncAlgebra, ApplyMonotoneStream, NotProved, StreamMapFuncAlgebra, ValidCommutativityFor,
38 ValidIdempotenceFor, ValidMutBorrowCommutativityFor, ValidMutBorrowIdempotenceFor,
39 ValidMutCommutativityFor, ValidMutIdempotenceFor,
40};
41
42pub mod networking;
43
44/// A trait implemented by valid ordering markers ([`TotalOrder`] and [`NoOrder`]).
45#[sealed::sealed]
46pub trait Ordering:
47 MinOrder<Self, Min = Self> + MinOrder<TotalOrder, Min = Self> + MinOrder<NoOrder, Min = NoOrder>
48{
49 /// The [`StreamOrder`] corresponding to this type.
50 const ORDERING_KIND: StreamOrder;
51}
52
53/// Marks the stream as being totally ordered, which means that there are
54/// no sources of non-determinism (other than intentional ones) that will
55/// affect the order of elements.
56pub enum TotalOrder {}
57
58#[sealed::sealed]
59impl Ordering for TotalOrder {
60 const ORDERING_KIND: StreamOrder = StreamOrder::TotalOrder;
61}
62
63/// Marks the stream as having no order, which means that the order of
64/// elements may be affected by non-determinism.
65///
66/// This restricts certain operators, such as `fold` and `reduce`, to only
67/// be used with commutative aggregation functions.
68pub enum NoOrder {}
69
70#[sealed::sealed]
71impl Ordering for NoOrder {
72 const ORDERING_KIND: StreamOrder = StreamOrder::NoOrder;
73}
74
75/// Marker trait for an [`Ordering`] that is available when `Self` is a weaker guarantee than
76/// `Other`, which means that a stream with `Other` guarantees can be safely converted to
77/// have `Self` guarantees instead.
78#[sealed::sealed]
79pub trait WeakerOrderingThan<Other: ?Sized>: Ordering {}
80#[sealed::sealed]
81impl<O, O2: Ordering> WeakerOrderingThan<O2> for O where O: Ordering + MinOrder<O2, Min = O> {}
82
83/// Helper trait for determining the weakest of two orderings.
84#[sealed::sealed]
85pub trait MinOrder<Other: ?Sized> {
86 /// The weaker of the two orderings.
87 type Min: Ordering;
88}
89
90#[sealed::sealed]
91impl<O: Ordering> MinOrder<O> for TotalOrder {
92 type Min = O;
93}
94
95#[sealed::sealed]
96impl<O: Ordering> MinOrder<O> for NoOrder {
97 type Min = NoOrder;
98}
99
100/// A trait implemented by valid retries markers ([`ExactlyOnce`] and [`AtLeastOnce`]).
101#[sealed::sealed]
102pub trait Retries:
103 MinRetries<Self, Min = Self>
104 + MinRetries<ExactlyOnce, Min = Self>
105 + MinRetries<AtLeastOnce, Min = AtLeastOnce>
106{
107 /// The [`StreamRetry`] corresponding to this type.
108 const RETRIES_KIND: StreamRetry;
109}
110
111/// Marks the stream as having deterministic message cardinality, with no
112/// possibility of duplicates.
113pub enum ExactlyOnce {}
114
115#[sealed::sealed]
116impl Retries for ExactlyOnce {
117 const RETRIES_KIND: StreamRetry = StreamRetry::ExactlyOnce;
118}
119
120/// Marks the stream as having non-deterministic message cardinality, which
121/// means that duplicates may occur, but messages will not be dropped.
122pub enum AtLeastOnce {}
123
124#[sealed::sealed]
125impl Retries for AtLeastOnce {
126 const RETRIES_KIND: StreamRetry = StreamRetry::AtLeastOnce;
127}
128
129/// Marker trait for a [`Retries`] that is available when `Self` is a weaker guarantee than
130/// `Other`, which means that a stream with `Other` guarantees can be safely converted to
131/// have `Self` guarantees instead.
132#[sealed::sealed]
133pub trait WeakerRetryThan<Other: ?Sized>: Retries {}
134#[sealed::sealed]
135impl<R, R2: Retries> WeakerRetryThan<R2> for R where R: Retries + MinRetries<R2, Min = R> {}
136
137/// Helper trait for determining the weakest of two retry guarantees.
138#[sealed::sealed]
139pub trait MinRetries<Other: ?Sized> {
140 /// The weaker of the two retry guarantees.
141 type Min: Retries + WeakerRetryThan<Self> + WeakerRetryThan<Other>;
142}
143
144#[sealed::sealed]
145impl<R: Retries> MinRetries<R> for ExactlyOnce {
146 type Min = R;
147}
148
149#[sealed::sealed]
150impl<R: Retries> MinRetries<R> for AtLeastOnce {
151 type Min = AtLeastOnce;
152}
153
154#[sealed::sealed]
155#[diagnostic::on_unimplemented(
156 message = "the input stream must be totally ordered (`TotalOrder`), but has order `{Self}`",
157 label = "required here",
158 note = "strengthen the order upstream, or consider a different API",
159 note = "to intentionally process the stream by observing a non-deterministic (shuffled) order of elements, use `.assume_ordering`; this introduces non-determinism, so avoid it unless necessary"
160)]
161/// Marker trait that is implemented for the [`TotalOrder`] ordering guarantee.
162pub trait IsOrdered: Ordering {}
163
164#[sealed::sealed]
165#[diagnostic::do_not_recommend]
166impl IsOrdered for TotalOrder {}
167
168#[sealed::sealed]
169#[diagnostic::on_unimplemented(
170 message = "the input stream must be exactly-once (`ExactlyOnce`), but has retries `{Self}`",
171 label = "required here",
172 note = "strengthen the retries guarantee upstream, or consider a different API",
173 note = "to intentionally process the stream by observing non-deterministic (randomly duplicated) retries, use `.assume_retries`; this introduces non-determinism, so avoid it unless necessary"
174)]
175/// Marker trait that is implemented for the [`ExactlyOnce`] retries guarantee.
176pub trait IsExactlyOnce: Retries {}
177
178#[sealed::sealed]
179#[diagnostic::do_not_recommend]
180impl IsExactlyOnce for ExactlyOnce {}
181
182/// Streaming sequence of elements with type `Type`.
183///
184/// This live collection represents a growing sequence of elements, with new elements being
185/// asynchronously appended to the end of the sequence. This can be used to model the arrival
186/// of network input, such as API requests, or streaming ingestion.
187///
188/// By default, all streams have deterministic ordering and each element is materialized exactly
189/// once. But streams can also capture non-determinism via the `Order` and `Retries` type
190/// parameters. When the ordering / retries guarantee is relaxed, fewer APIs will be available
191/// on the stream. For example, if the stream is unordered, you cannot invoke [`Stream::first`].
192///
193/// Type Parameters:
194/// - `Type`: the type of elements in the stream
195/// - `Loc`: the location where the stream is being materialized
196/// - `Bound`: the boundedness of the stream, which is either [`Bounded`] or [`Unbounded`]
197/// - `Order`: the ordering of the stream, which is either [`TotalOrder`] or [`NoOrder`]
198/// (default is [`TotalOrder`])
199/// - `Retries`: the retry guarantee of the stream, which is either [`ExactlyOnce`] or
200/// [`AtLeastOnce`] (default is [`ExactlyOnce`])
201pub struct Stream<
202 Type,
203 Loc,
204 Bound: Boundedness = Unbounded,
205 Order: Ordering = TotalOrder,
206 Retry: Retries = ExactlyOnce,
207> {
208 pub(crate) location: Loc,
209 pub(crate) ir_node: Rc<RefCell<HydroNode>>,
210 pub(crate) flow_state: FlowState,
211
212 _phantom: PhantomData<(Type, Loc, Bound, Order, Retry)>,
213}
214
215impl<T, L, B: Boundedness, O: Ordering, R: Retries> Drop for Stream<T, L, B, O, R> {
216 fn drop(&mut self) {
217 let ir_node = self.ir_node.replace(HydroNode::Placeholder);
218 if !matches!(ir_node, HydroNode::Placeholder) && !ir_node.is_shared_with_others() {
219 self.flow_state.borrow_mut().try_push_root(HydroRoot::Null {
220 input: Box::new(ir_node),
221 op_metadata: HydroIrOpMetadata::new(),
222 });
223 }
224 }
225}
226
227impl<'a, T, L, O: Ordering, R: Retries> From<Stream<T, L, Bounded, O, R>>
228 for Stream<T, L, Unbounded, O, R>
229where
230 L: Location<'a>,
231{
232 fn from(stream: Stream<T, L, Bounded, O, R>) -> Stream<T, L, Unbounded, O, R> {
233 let new_meta = stream
234 .location
235 .new_node_metadata(Stream::<T, L, Unbounded, O, R>::collection_kind());
236
237 let flow_state = stream.flow_state.clone();
238 Stream {
239 location: stream.location.clone(),
240 ir_node: super::tracked_ir_node(
241 &flow_state,
242 HydroNode::Cast {
243 inner: Box::new(stream.ir_node.replace(HydroNode::Placeholder)),
244 metadata: new_meta,
245 },
246 ),
247 flow_state,
248 _phantom: PhantomData,
249 }
250 }
251}
252
253impl<'a, T, L, B: Boundedness, R: Retries> From<Stream<T, L, B, TotalOrder, R>>
254 for Stream<T, L, B, NoOrder, R>
255where
256 L: Location<'a>,
257{
258 fn from(stream: Stream<T, L, B, TotalOrder, R>) -> Stream<T, L, B, NoOrder, R> {
259 stream.weaken_ordering()
260 }
261}
262
263impl<'a, T, L, B: Boundedness, O: Ordering> From<Stream<T, L, B, O, ExactlyOnce>>
264 for Stream<T, L, B, O, AtLeastOnce>
265where
266 L: Location<'a>,
267{
268 fn from(stream: Stream<T, L, B, O, ExactlyOnce>) -> Stream<T, L, B, O, AtLeastOnce> {
269 stream.weaken_retries()
270 }
271}
272
273impl<'a, T, L, O: Ordering, R: Retries> DeferTick for Stream<T, Tick<L>, Bounded, O, R>
274where
275 L: Location<'a>,
276{
277 fn defer_tick(self) -> Self {
278 Stream::defer_tick(self)
279 }
280}
281
282impl<'a, T, L, O: Ordering, R: Retries> CycleCollection<'a, TickCycle>
283 for Stream<T, Tick<L>, Bounded, O, R>
284where
285 L: Location<'a>,
286{
287 type Location = Tick<L>;
288
289 fn create_source(cycle_id: CycleId, location: Tick<L>) -> Self {
290 Stream::new(
291 location.clone(),
292 HydroNode::CycleSource {
293 cycle_id,
294 metadata: location.new_node_metadata(Self::collection_kind()),
295 },
296 )
297 }
298}
299
300impl<'a, T, L, O: Ordering, R: Retries> CycleCollectionWithInitial<'a, TickCycle>
301 for Stream<T, Tick<L>, Bounded, O, R>
302where
303 L: Location<'a>,
304{
305 type Location = Tick<L>;
306
307 fn location(&self) -> &Self::Location {
308 self.location()
309 }
310
311 fn create_source_with_initial(cycle_id: CycleId, initial: Self, location: Tick<L>) -> Self {
312 let from_previous_tick: Stream<T, Tick<L>, Bounded, O, R> = Stream::new(
313 location.clone(),
314 HydroNode::DeferTick {
315 input: Box::new(HydroNode::CycleSource {
316 cycle_id,
317 metadata: location.new_node_metadata(Self::collection_kind()),
318 }),
319 metadata: location.new_node_metadata(Self::collection_kind()),
320 },
321 );
322
323 from_previous_tick.chain(initial.filter_if(location.optional_first_tick(q!(())).is_some()))
324 }
325}
326
327impl<'a, T, L, O: Ordering, R: Retries> ReceiverComplete<'a, TickCycle>
328 for Stream<T, Tick<L>, Bounded, O, R>
329where
330 L: Location<'a>,
331{
332 fn complete(self, cycle_id: CycleId, expected_location: LocationId) {
333 assert_eq!(
334 Location::id(&self.location),
335 expected_location,
336 "locations do not match"
337 );
338 self.location
339 .flow_state()
340 .borrow_mut()
341 .push_root(HydroRoot::CycleSink {
342 cycle_id,
343 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
344 op_metadata: HydroIrOpMetadata::new(),
345 });
346 }
347}
348
349impl<'a, T, L, B: Boundedness, O: Ordering, R: Retries> CycleCollection<'a, ForwardRef>
350 for Stream<T, L, B, O, R>
351where
352 L: Location<'a>,
353{
354 type Location = L;
355
356 fn create_source(cycle_id: CycleId, location: L) -> Self {
357 Stream::new(
358 location.clone(),
359 HydroNode::CycleSource {
360 cycle_id,
361 metadata: location.new_node_metadata(Self::collection_kind()),
362 },
363 )
364 }
365}
366
367impl<'a, T, L, B: Boundedness, O: Ordering, R: Retries> ReceiverComplete<'a, ForwardRef>
368 for Stream<T, L, B, O, R>
369where
370 L: Location<'a>,
371{
372 fn complete(self, cycle_id: CycleId, expected_location: LocationId) {
373 assert_eq!(
374 Location::id(&self.location),
375 expected_location,
376 "locations do not match"
377 );
378 self.location
379 .flow_state()
380 .borrow_mut()
381 .push_root(HydroRoot::CycleSink {
382 cycle_id,
383 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
384 op_metadata: HydroIrOpMetadata::new(),
385 });
386 }
387}
388
389impl<'a, T, L, B: Boundedness, O: Ordering, R: Retries> Clone for Stream<T, L, B, O, R>
390where
391 T: Clone,
392 L: Location<'a>,
393{
394 fn clone(&self) -> Self {
395 if !matches!(self.ir_node.borrow().deref(), HydroNode::Tee { .. }) {
396 let orig_ir_node = self.ir_node.replace(HydroNode::Placeholder);
397 *self.ir_node.borrow_mut() = HydroNode::Tee {
398 inner: SharedNode(Rc::new(RefCell::new(orig_ir_node))),
399 metadata: self.location.new_node_metadata(Self::collection_kind()),
400 };
401 }
402
403 let HydroNode::Tee { inner, metadata } = &*self.ir_node.borrow() else {
404 unreachable!()
405 };
406 Stream {
407 location: self.location.clone(),
408 flow_state: self.flow_state.clone(),
409 ir_node: super::tracked_ir_node(
410 &self.flow_state,
411 HydroNode::Tee {
412 inner: SharedNode(inner.0.clone()),
413 metadata: metadata.clone(),
414 },
415 ),
416 _phantom: PhantomData,
417 }
418 }
419}
420
421impl<'a, T, L, B: Boundedness, O: Ordering, R: Retries> Stream<T, L, B, O, R>
422where
423 L: Location<'a>,
424{
425 pub(crate) fn new(location: L, ir_node: HydroNode) -> Self {
426 debug_assert_eq!(ir_node.metadata().location_id, Location::id(&location));
427 debug_assert_eq!(ir_node.metadata().collection_kind, Self::collection_kind());
428
429 let flow_state = location.flow_state().clone();
430 let ir_node = super::tracked_ir_node(&flow_state, ir_node);
431 Stream {
432 location,
433 flow_state,
434 ir_node,
435 _phantom: PhantomData,
436 }
437 }
438
439 /// Returns the [`Location`] where this stream is being materialized.
440 pub fn location(&self) -> &L {
441 &self.location
442 }
443
444 /// Creates a shared reference handle to this stream's handoff buffer that can be captured
445 /// inside `q!()` closures. The handle resolves to `&Vec<T>` at runtime.
446 ///
447 /// The stream must be bounded, otherwise reading it would be non-deterministic.
448 pub fn by_ref(&self) -> crate::handoff_ref::StreamRef<'a, '_, T, L, B>
449 where
450 B: IsBounded,
451 {
452 crate::handoff_ref::StreamRef::new(&self.ir_node)
453 }
454
455 /// Returns a mutable reference handle to this stream's handoff buffer that can be captured
456 /// inside `q!()` closures. The handle resolves to `&mut Vec<T>` at runtime.
457 pub fn by_mut(&self) -> crate::handoff_ref::StreamMut<'a, '_, T, L, B>
458 where
459 B: IsBounded,
460 {
461 crate::handoff_ref::StreamMut::new(&self.ir_node)
462 }
463
464 /// Weakens the consistency of this live collection to not guarantee any consistency across
465 /// cluster members (if this collection is on a cluster).
466 pub fn weaken_consistency(self) -> Stream<T, L::DropConsistency, B, O, R>
467 where
468 L: Location<'a>,
469 {
470 if L::consistency()
471 .is_none_or(|c| c == crate::location::dynamic::ClusterConsistency::NoConsistency)
472 {
473 // already no consistency
474 Stream::new(
475 self.location.drop_consistency(),
476 self.ir_node.replace(HydroNode::Placeholder),
477 )
478 } else {
479 Stream::new(
480 self.location.drop_consistency(),
481 HydroNode::Cast {
482 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
483 metadata: self.location.drop_consistency().new_node_metadata(Stream::<
484 T,
485 L::DropConsistency,
486 B,
487 O,
488 R,
489 >::collection_kind(
490 )),
491 },
492 )
493 }
494 }
495
496 /// Casts this live collection to have the consistency guarantees specified in the given
497 /// location type parameter. The developer must ensure that the strengthened consistency
498 /// is actually guaranteed, via the proof field (see [`crate::prelude::manual_proof`]).
499 pub fn assert_has_consistency_of<L2: Location<'a, DropConsistency = L::DropConsistency>>(
500 self,
501 _proof: impl crate::properties::ConsistencyProof,
502 ) -> Stream<T, L2, B, O, R>
503 where
504 L: Location<'a>,
505 {
506 if L::consistency() == L2::consistency() {
507 Stream::new(
508 self.location.with_consistency_of(),
509 self.ir_node.replace(HydroNode::Placeholder),
510 )
511 } else {
512 Stream::new(
513 self.location.with_consistency_of(),
514 HydroNode::AssertIsConsistent {
515 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
516 trusted: false,
517 metadata: self
518 .location
519 .clone()
520 .with_consistency_of::<L2>()
521 .new_node_metadata(Stream::<T, L2, B, O, R>::collection_kind()),
522 },
523 )
524 }
525 }
526
527 pub(crate) fn assert_has_consistency_of_trusted<
528 L2: Location<'a, DropConsistency = L::DropConsistency>,
529 >(
530 self,
531 _proof: impl crate::properties::ConsistencyProof,
532 ) -> Stream<T, L2, B, O, R>
533 where
534 L: Location<'a>,
535 {
536 if L::consistency() == L2::consistency() {
537 Stream::new(
538 self.location.with_consistency_of(),
539 self.ir_node.replace(HydroNode::Placeholder),
540 )
541 } else {
542 Stream::new(
543 self.location.with_consistency_of(),
544 HydroNode::AssertIsConsistent {
545 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
546 trusted: true,
547 metadata: self
548 .location
549 .clone()
550 .with_consistency_of::<L2>()
551 .new_node_metadata(Stream::<T, L2, B, O, R>::collection_kind()),
552 },
553 )
554 }
555 }
556
557 pub(crate) fn collection_kind() -> CollectionKind {
558 CollectionKind::Stream {
559 bound: B::BOUND_KIND,
560 order: O::ORDERING_KIND,
561 retry: R::RETRIES_KIND,
562 element_type: quote_type::<T>().into(),
563 }
564 }
565
566 /// Produces a stream based on invoking `f` on each element.
567 /// If you do not want to modify the stream and instead only want to view
568 /// each item use [`Stream::inspect`] instead.
569 ///
570 /// If the input stream is unordered **and** `f` mutably captures state (such as a
571 /// [`Singleton::by_mut`](crate::live_collections::singleton::Singleton::by_mut)
572 /// reference), `f` must be proven **commutative**: processing any two elements in
573 /// either order must leave the mutably-captured state in the same final value *and*
574 /// produce the same multiset of outputs. In particular, outputs must not expose the
575 /// processing order (e.g. emitting a running total is not commutative even though
576 /// addition is).
577 ///
578 /// Similarly, if the input stream has retries (`AtLeastOnce`) **and** `f` mutably
579 /// captures state, `f` must be proven **idempotent**: processing an element twice in
580 /// a row must leave the mutably-captured state in the same final value as processing
581 /// it once *and* return the same output both times. In particular, outputs must not
582 /// expose whether an element is a retry (e.g. emitting "is this a new maximum?" is not
583 /// idempotent even though `max` is).
584 ///
585 /// # Example
586 /// ```rust
587 /// # #[cfg(feature = "deploy")] {
588 /// # use hydro_lang::prelude::*;
589 /// # use futures::StreamExt;
590 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
591 /// let words = process.source_iter(q!(vec!["hello", "world"]));
592 /// words.map(q!(|x| x.to_uppercase()))
593 /// # }, |mut stream| async move {
594 /// # for w in vec!["HELLO", "WORLD"] {
595 /// # assert_eq!(stream.next().await.unwrap(), w);
596 /// # }
597 /// # }));
598 /// # }
599 /// ```
600 pub fn map<U, F, C, I, const WAS_MUT: bool>(
601 self,
602 f: impl IntoQuotedMut<
603 'a,
604 F,
605 OperatorContext<L, B>,
606 StreamMapFuncAlgebra<T, B, C, I, L::SimHookScope>,
607 >,
608 ) -> Stream<U, L, B, O, R>
609 where
610 F: FnMut(T) -> U + 'a,
611 C: ValidMutCommutativityFor<F, T, U, O, WAS_MUT>,
612 I: ValidMutIdempotenceFor<F, T, U, R, WAS_MUT>,
613 {
614 let f = crate::handoff_ref::with_ref_capture(|| {
615 let (expr, proof) =
616 f.splice_fnmut1_ctx_props(&OperatorContext::<L, B>::new(&self.location));
617 proof.register_proof(&expr);
618 expr.into()
619 });
620 Stream::new(
621 self.location.clone(),
622 HydroNode::Map {
623 f,
624 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
625 metadata: self
626 .location
627 .new_node_metadata(Stream::<U, L, B, O, R>::collection_kind()),
628 },
629 )
630 }
631
632 /// For each item `i` in the input stream, transform `i` using `f` and then treat the
633 /// result as an [`Iterator`] to produce items one by one. The implementation for [`Iterator`]
634 /// for the output type `U` must produce items in a **deterministic** order.
635 ///
636 /// For example, `U` could be a `Vec`, but not a `HashSet`. If the order of the items in `U` is
637 /// not deterministic, use [`Stream::flat_map_unordered`] instead.
638 ///
639 /// # Example
640 /// ```rust
641 /// # #[cfg(feature = "deploy")] {
642 /// # use hydro_lang::prelude::*;
643 /// # use futures::StreamExt;
644 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
645 /// process
646 /// .source_iter(q!(vec![vec![1, 2], vec![3, 4]]))
647 /// .flat_map_ordered(q!(|x| x))
648 /// # }, |mut stream| async move {
649 /// // 1, 2, 3, 4
650 /// # for w in (1..5) {
651 /// # assert_eq!(stream.next().await.unwrap(), w);
652 /// # }
653 /// # }));
654 /// # }
655 /// ```
656 pub fn flat_map_ordered<U, I, F, C, Idemp, const WAS_MUT: bool>(
657 self,
658 f: impl IntoQuotedMut<
659 'a,
660 F,
661 OperatorContext<L, B>,
662 StreamMapFuncAlgebra<T, B, C, Idemp, L::SimHookScope>,
663 >,
664 ) -> Stream<U, L, B, O, R>
665 where
666 I: IntoIterator<Item = U>,
667 F: FnMut(T) -> I + 'a,
668 C: ValidMutCommutativityFor<F, T, I, O, WAS_MUT>,
669 Idemp: ValidMutIdempotenceFor<F, T, I, R, WAS_MUT>,
670 {
671 let f = crate::handoff_ref::with_ref_capture(|| {
672 let (expr, proof) =
673 f.splice_fnmut1_ctx_props(&OperatorContext::<L, B>::new(&self.location));
674 proof.register_proof(&expr);
675 expr.into()
676 });
677 Stream::new(
678 self.location.clone(),
679 HydroNode::FlatMap {
680 f,
681 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
682 metadata: self
683 .location
684 .new_node_metadata(Stream::<U, L, B, O, R>::collection_kind()),
685 },
686 )
687 }
688
689 /// Like [`Stream::flat_map_ordered`], but allows the implementation of [`Iterator`]
690 /// for the output type `U` to produce items in any order.
691 ///
692 /// # Example
693 /// ```rust
694 /// # #[cfg(feature = "deploy")] {
695 /// # use hydro_lang::{prelude::*, live_collections::stream::{NoOrder, ExactlyOnce}};
696 /// # use futures::StreamExt;
697 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test::<_, _, _, NoOrder, ExactlyOnce>(|process| {
698 /// process
699 /// .source_iter(q!(vec![
700 /// std::collections::HashSet::<i32>::from_iter(vec![1, 2]),
701 /// std::collections::HashSet::from_iter(vec![3, 4]),
702 /// ]))
703 /// .flat_map_unordered(q!(|x| x))
704 /// # }, |mut stream| async move {
705 /// // 1, 2, 3, 4, but in no particular order
706 /// # let mut results = Vec::new();
707 /// # for w in (1..5) {
708 /// # results.push(stream.next().await.unwrap());
709 /// # }
710 /// # results.sort();
711 /// # assert_eq!(results, vec![1, 2, 3, 4]);
712 /// # }));
713 /// # }
714 /// ```
715 pub fn flat_map_unordered<U, I, F, C, Idemp, const WAS_MUT: bool>(
716 self,
717 f: impl IntoQuotedMut<
718 'a,
719 F,
720 OperatorContext<L, B>,
721 StreamMapFuncAlgebra<T, B, C, Idemp, L::SimHookScope>,
722 >,
723 ) -> Stream<U, L, B, NoOrder, R>
724 where
725 I: IntoIterator<Item = U>,
726 F: FnMut(T) -> I + 'a,
727 C: ValidMutCommutativityFor<F, T, I, O, WAS_MUT>,
728 Idemp: ValidMutIdempotenceFor<F, T, I, R, WAS_MUT>,
729 {
730 let f = crate::handoff_ref::with_ref_capture(|| {
731 let (expr, proof) =
732 f.splice_fnmut1_ctx_props(&OperatorContext::<L, B>::new(&self.location));
733 proof.register_proof(&expr);
734 expr.into()
735 });
736 Stream::new(
737 self.location.clone(),
738 HydroNode::FlatMap {
739 f,
740 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
741 metadata: self
742 .location
743 .new_node_metadata(Stream::<U, L, B, NoOrder, R>::collection_kind()),
744 },
745 )
746 }
747
748 /// For each item `i` in the input stream, treat `i` as an [`Iterator`] and produce its items one by one.
749 /// The implementation for [`Iterator`] for the element type `T` must produce items in a **deterministic** order.
750 ///
751 /// For example, `T` could be a `Vec`, but not a `HashSet`. If the order of the items in `T` is
752 /// not deterministic, use [`Stream::flatten_unordered`] instead.
753 ///
754 /// ```rust
755 /// # #[cfg(feature = "deploy")] {
756 /// # use hydro_lang::prelude::*;
757 /// # use futures::StreamExt;
758 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
759 /// process
760 /// .source_iter(q!(vec![vec![1, 2], vec![3, 4]]))
761 /// .flatten_ordered()
762 /// # }, |mut stream| async move {
763 /// // 1, 2, 3, 4
764 /// # for w in (1..5) {
765 /// # assert_eq!(stream.next().await.unwrap(), w);
766 /// # }
767 /// # }));
768 /// # }
769 /// ```
770 pub fn flatten_ordered<U>(self) -> Stream<U, L, B, O, R>
771 where
772 T: IntoIterator<Item = U>,
773 {
774 self.flat_map_ordered(q!(|d| d))
775 }
776
777 /// Like [`Stream::flatten_ordered`], but allows the implementation of [`Iterator`]
778 /// for the element type `T` to produce items in any order.
779 ///
780 /// # Example
781 /// ```rust
782 /// # #[cfg(feature = "deploy")] {
783 /// # use hydro_lang::{prelude::*, live_collections::stream::{NoOrder, ExactlyOnce}};
784 /// # use futures::StreamExt;
785 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test::<_, _, _, NoOrder, ExactlyOnce>(|process| {
786 /// process
787 /// .source_iter(q!(vec![
788 /// std::collections::HashSet::<i32>::from_iter(vec![1, 2]),
789 /// std::collections::HashSet::from_iter(vec![3, 4]),
790 /// ]))
791 /// .flatten_unordered()
792 /// # }, |mut stream| async move {
793 /// // 1, 2, 3, 4, but in no particular order
794 /// # let mut results = Vec::new();
795 /// # for w in (1..5) {
796 /// # results.push(stream.next().await.unwrap());
797 /// # }
798 /// # results.sort();
799 /// # assert_eq!(results, vec![1, 2, 3, 4]);
800 /// # }));
801 /// # }
802 /// ```
803 pub fn flatten_unordered<U>(self) -> Stream<U, L, B, NoOrder, R>
804 where
805 T: IntoIterator<Item = U>,
806 {
807 self.flat_map_unordered(q!(|d| d))
808 }
809
810 /// For each item in the input stream, apply `f` to produce a [`futures::stream::Stream`],
811 /// then emit the elements of that stream one by one. When the inner stream yields
812 /// `Pending`, this operator yields as well.
813 pub fn flat_map_stream_blocking<U, S, F, C, Idemp, const WAS_MUT: bool>(
814 self,
815 f: impl IntoQuotedMut<
816 'a,
817 F,
818 OperatorContext<L, B>,
819 StreamMapFuncAlgebra<T, B, C, Idemp, L::SimHookScope>,
820 >,
821 ) -> Stream<U, L, B, O, R>
822 where
823 S: futures::Stream<Item = U>,
824 F: FnMut(T) -> S + 'a,
825 C: ValidMutCommutativityFor<F, T, S, O, WAS_MUT>,
826 Idemp: ValidMutIdempotenceFor<F, T, S, R, WAS_MUT>,
827 {
828 let f = crate::handoff_ref::with_ref_capture(|| {
829 let (expr, proof) =
830 f.splice_fnmut1_ctx_props(&OperatorContext::<L, B>::new(&self.location));
831 proof.register_proof(&expr);
832 expr.into()
833 });
834 Stream::new(
835 self.location.clone(),
836 HydroNode::FlatMapStreamBlocking {
837 f,
838 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
839 metadata: self
840 .location
841 .new_node_metadata(Stream::<U, L, B, O, R>::collection_kind()),
842 },
843 )
844 }
845
846 /// For each item in the input stream, treat it as a [`futures::stream::Stream`] and
847 /// emit its elements one by one. When the inner stream yields `Pending`, this operator
848 /// yields as well.
849 pub fn flatten_stream_blocking<U>(self) -> Stream<U, L, B, O, R>
850 where
851 T: futures::Stream<Item = U>,
852 {
853 self.flat_map_stream_blocking(q!(|d| d))
854 }
855
856 /// Creates a stream containing only the elements of the input stream that satisfy a predicate
857 /// `f`, preserving the order of the elements.
858 ///
859 /// The closure `f` receives a reference `&T` rather than an owned value `T` because filtering does
860 /// not modify or take ownership of the values. If you need to modify the values while filtering
861 /// use [`Stream::filter_map`] instead.
862 ///
863 /// If the input stream is unordered **and** `f` mutably captures state, `f` must be
864 /// proven **commutative**: processing any two elements in either order must leave
865 /// the mutably-captured state in the same final value *and* retain the same multiset
866 /// of elements. In particular, the decision for each element must not depend on the
867 /// processing order: a stateful predicate like a rate limiter is not commutative —
868 /// its budget converges either way, but *which* element passes depends on the order.
869 ///
870 /// Similarly, if the input stream has retries (`AtLeastOnce`) **and** `f` mutably
871 /// captures state, `f` must be proven **idempotent**: processing an element twice in
872 /// a row must leave the mutably-captured state in the same final value as processing
873 /// it once *and* make the same decision both times. In particular, the decision must
874 /// not depend on whether the element was seen before.
875 ///
876 /// # Example
877 /// ```rust
878 /// # #[cfg(feature = "deploy")] {
879 /// # use hydro_lang::prelude::*;
880 /// # use futures::StreamExt;
881 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
882 /// process
883 /// .source_iter(q!(vec![1, 2, 3, 4]))
884 /// .filter(q!(|&x| x > 2))
885 /// # }, |mut stream| async move {
886 /// // 3, 4
887 /// # for w in (3..5) {
888 /// # assert_eq!(stream.next().await.unwrap(), w);
889 /// # }
890 /// # }));
891 /// # }
892 /// ```
893 pub fn filter<F, C, Idemp, const WAS_MUT: bool>(
894 self,
895 f: impl IntoQuotedMut<
896 'a,
897 F,
898 OperatorContext<L, B>,
899 StreamMapFuncAlgebra<T, B, C, Idemp, L::SimHookScope>,
900 >,
901 ) -> Self
902 where
903 F: FnMut(&T) -> bool + 'a,
904 C: ValidMutBorrowCommutativityFor<F, T, bool, O, WAS_MUT>,
905 Idemp: ValidMutBorrowIdempotenceFor<F, T, bool, R, WAS_MUT>,
906 {
907 let f = crate::handoff_ref::with_ref_capture(|| {
908 let (expr, proof) =
909 f.splice_fnmut1_borrow_ctx_props(&OperatorContext::<L, B>::new(&self.location));
910 proof.register_proof(&expr);
911 expr.into()
912 });
913 Stream::new(
914 self.location.clone(),
915 HydroNode::Filter {
916 f,
917 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
918 metadata: self.location.new_node_metadata(Self::collection_kind()),
919 },
920 )
921 }
922
923 /// Splits the stream into two streams based on a predicate, without cloning elements.
924 ///
925 /// Elements for which `f` returns `true` are sent to the first output stream,
926 /// and elements for which `f` returns `false` are sent to the second output stream.
927 ///
928 /// Unlike using `filter` twice, this only evaluates the predicate once per element
929 /// and does not require `T: Clone`.
930 ///
931 /// The closure `f` receives a reference `&T` rather than an owned value `T` because
932 /// the predicate is only used for routing; the element itself is moved to the
933 /// appropriate output stream.
934 ///
935 /// If the input stream has retries (`AtLeastOnce`) **and** `f` mutably captures
936 /// state, `f` must be proven **idempotent**: processing an element twice in a row
937 /// must leave the mutably-captured state in the same final value as processing it
938 /// once *and* make the same decision both times (otherwise a retried element would
939 /// appear in the *other* output).
940 ///
941 /// # Example
942 /// ```rust
943 /// # #[cfg(feature = "deploy")] {
944 /// # use hydro_lang::prelude::*;
945 /// # use hydro_lang::live_collections::stream::{NoOrder, ExactlyOnce};
946 /// # use futures::StreamExt;
947 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test::<_, _, _, NoOrder, ExactlyOnce>(|process| {
948 /// let numbers: Stream<_, _, Unbounded> = process.source_iter(q!(vec![1, 2, 3, 4, 5, 6])).into();
949 /// let (evens, odds) = numbers.partition(q!(|&x| x % 2 == 0));
950 /// // evens: 2, 4, 6 tagged with true; odds: 1, 3, 5 tagged with false
951 /// evens.map(q!(|x| (x, true)))
952 /// .merge_unordered(odds.map(q!(|x| (x, false))))
953 /// # }, |mut stream| async move {
954 /// # let mut results = Vec::new();
955 /// # for _ in 0..6 {
956 /// # results.push(stream.next().await.unwrap());
957 /// # }
958 /// # results.sort();
959 /// # assert_eq!(results, vec![(1, false), (2, true), (3, false), (4, true), (5, false), (6, true)]);
960 /// # }));
961 /// # }
962 /// ```
963 pub fn partition<F, C, Idemp, const WAS_MUT: bool>(
964 self,
965 f: impl IntoQuotedMut<
966 'a,
967 F,
968 OperatorContext<L, B>,
969 StreamMapFuncAlgebra<T, B, C, Idemp, L::SimHookScope>,
970 >,
971 ) -> (Stream<T, L, B, O, R>, Stream<T, L, B, O, R>)
972 where
973 F: FnMut(&T) -> bool + 'a,
974 C: ValidMutBorrowCommutativityFor<F, T, bool, O, WAS_MUT>,
975 Idemp: ValidMutBorrowIdempotenceFor<F, T, bool, R, WAS_MUT>,
976 {
977 let f = crate::handoff_ref::with_ref_capture(|| {
978 let (expr, proof) =
979 f.splice_fnmut1_borrow_ctx_props(&OperatorContext::<L, B>::new(&self.location));
980 proof.register_proof(&expr);
981 expr.into()
982 });
983 let shared = Rc::new(RefCell::new(HydroNode::PartitionShared {
984 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
985 f,
986 metadata: self.location.new_node_metadata(Self::collection_kind()),
987 }));
988
989 let true_stream = Stream::new(
990 self.location.clone(),
991 HydroNode::PartitionSide {
992 inner: SharedNode(Rc::clone(&shared)),
993 is_true: true,
994 metadata: self.location.new_node_metadata(Self::collection_kind()),
995 },
996 );
997
998 let false_stream = Stream::new(
999 self.location.clone(),
1000 HydroNode::PartitionSide {
1001 inner: SharedNode(shared),
1002 is_true: false,
1003 metadata: self.location.new_node_metadata(Self::collection_kind()),
1004 },
1005 );
1006
1007 (true_stream, false_stream)
1008 }
1009
1010 /// An operator that both filters and maps. It yields only the items for which the supplied closure `f` returns `Some(value)`.
1011 ///
1012 /// # Example
1013 /// ```rust
1014 /// # #[cfg(feature = "deploy")] {
1015 /// # use hydro_lang::prelude::*;
1016 /// # use futures::StreamExt;
1017 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1018 /// process
1019 /// .source_iter(q!(vec!["1", "hello", "world", "2"]))
1020 /// .filter_map(q!(|s| s.parse::<usize>().ok()))
1021 /// # }, |mut stream| async move {
1022 /// // 1, 2
1023 /// # for w in (1..3) {
1024 /// # assert_eq!(stream.next().await.unwrap(), w);
1025 /// # }
1026 /// # }));
1027 /// # }
1028 /// ```
1029 pub fn filter_map<U, F, C, Idemp, const WAS_MUT: bool>(
1030 self,
1031 f: impl IntoQuotedMut<
1032 'a,
1033 F,
1034 OperatorContext<L, B>,
1035 StreamMapFuncAlgebra<T, B, C, Idemp, L::SimHookScope>,
1036 >,
1037 ) -> Stream<U, L, B, O, R>
1038 where
1039 F: FnMut(T) -> Option<U> + 'a,
1040 C: ValidMutCommutativityFor<F, T, Option<U>, O, WAS_MUT>,
1041 Idemp: ValidMutIdempotenceFor<F, T, Option<U>, R, WAS_MUT>,
1042 {
1043 let f = crate::handoff_ref::with_ref_capture(|| {
1044 let (expr, proof) =
1045 f.splice_fnmut1_ctx_props(&OperatorContext::<L, B>::new(&self.location));
1046 proof.register_proof(&expr);
1047 expr.into()
1048 });
1049 Stream::new(
1050 self.location.clone(),
1051 HydroNode::FilterMap {
1052 f,
1053 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1054 metadata: self
1055 .location
1056 .new_node_metadata(Stream::<U, L, B, O, R>::collection_kind()),
1057 },
1058 )
1059 }
1060
1061 /// Generates a stream that maps each input element `i` to a tuple `(i, x)`,
1062 /// where `x` is the final value of `other`, a bounded [`Singleton`] or [`Optional`].
1063 /// If `other` is an empty [`Optional`], no values will be produced.
1064 ///
1065 /// # Example
1066 /// ```rust
1067 /// # #[cfg(feature = "deploy")] {
1068 /// # use hydro_lang::prelude::*;
1069 /// # use futures::StreamExt;
1070 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1071 /// let tick = process.tick();
1072 /// let batch = process
1073 /// .source_iter(q!(vec![1, 2, 3, 4]))
1074 /// .batch(&tick, nondet!(/** test */));
1075 /// let count = batch.clone().count(); // `count()` returns a singleton
1076 /// batch.cross_singleton(count).all_ticks()
1077 /// # }, |mut stream| async move {
1078 /// // (1, 4), (2, 4), (3, 4), (4, 4)
1079 /// # for w in vec![(1, 4), (2, 4), (3, 4), (4, 4)] {
1080 /// # assert_eq!(stream.next().await.unwrap(), w);
1081 /// # }
1082 /// # }));
1083 /// # }
1084 /// ```
1085 pub fn cross_singleton<O2>(
1086 self,
1087 other: impl Into<Optional<O2, L, Bounded>>,
1088 ) -> Stream<(T, O2), L, B, O, R>
1089 where
1090 O2: Clone,
1091 {
1092 let other: Optional<O2, L, Bounded> = other.into();
1093 check_matching_location(&self.location, &other.location);
1094
1095 Stream::new(
1096 self.location.clone(),
1097 HydroNode::CrossSingleton {
1098 left: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1099 right: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
1100 metadata: self
1101 .location
1102 .new_node_metadata(Stream::<(T, O2), L, B, O, R>::collection_kind()),
1103 },
1104 )
1105 }
1106
1107 /// Passes this stream through if the boolean signal is `true`, otherwise the output is empty.
1108 ///
1109 /// # Example
1110 /// ```rust
1111 /// # #[cfg(feature = "deploy")] {
1112 /// # use hydro_lang::prelude::*;
1113 /// # use futures::StreamExt;
1114 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1115 /// let tick = process.tick();
1116 /// // ticks are lazy by default, forces the second tick to run
1117 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1118 ///
1119 /// let signal = tick.optional_first_tick(q!(())).is_some(); // true on tick 1, false on tick 2
1120 /// let batch_first_tick = process
1121 /// .source_iter(q!(vec![1, 2, 3, 4]))
1122 /// .batch(&tick, nondet!(/** test */));
1123 /// let batch_second_tick = process
1124 /// .source_iter(q!(vec![5, 6, 7, 8]))
1125 /// .batch(&tick, nondet!(/** test */))
1126 /// .defer_tick();
1127 /// batch_first_tick.chain(batch_second_tick)
1128 /// .filter_if(signal)
1129 /// .all_ticks()
1130 /// # }, |mut stream| async move {
1131 /// // [1, 2, 3, 4]
1132 /// # for w in vec![1, 2, 3, 4] {
1133 /// # assert_eq!(stream.next().await.unwrap(), w);
1134 /// # }
1135 /// # }));
1136 /// # }
1137 /// ```
1138 pub fn filter_if(self, signal: Singleton<bool, L, Bounded>) -> Stream<T, L, B, O, R> {
1139 self.cross_singleton(signal.filter(q!(|b| *b)))
1140 .map(q!(|(d, _)| d))
1141 }
1142
1143 /// Passes this stream through if the argument (a [`Bounded`] [`Optional`]) is non-null, otherwise the output is empty.
1144 ///
1145 /// Useful for gating the release of elements based on a condition, such as only processing requests if you are the
1146 /// leader of a cluster.
1147 ///
1148 /// # Example
1149 /// ```rust
1150 /// # #[cfg(feature = "deploy")] {
1151 /// # use hydro_lang::prelude::*;
1152 /// # use futures::StreamExt;
1153 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1154 /// let tick = process.tick();
1155 /// // ticks are lazy by default, forces the second tick to run
1156 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1157 ///
1158 /// let batch_first_tick = process
1159 /// .source_iter(q!(vec![1, 2, 3, 4]))
1160 /// .batch(&tick, nondet!(/** test */));
1161 /// let batch_second_tick = process
1162 /// .source_iter(q!(vec![5, 6, 7, 8]))
1163 /// .batch(&tick, nondet!(/** test */))
1164 /// .defer_tick(); // appears on the second tick
1165 /// let some_on_first_tick = tick.optional_first_tick(q!(()));
1166 /// batch_first_tick.chain(batch_second_tick)
1167 /// .filter_if_some(some_on_first_tick)
1168 /// .all_ticks()
1169 /// # }, |mut stream| async move {
1170 /// // [1, 2, 3, 4]
1171 /// # for w in vec![1, 2, 3, 4] {
1172 /// # assert_eq!(stream.next().await.unwrap(), w);
1173 /// # }
1174 /// # }));
1175 /// # }
1176 /// ```
1177 #[deprecated(note = "use `filter_if` with `Optional::is_some()` instead")]
1178 pub fn filter_if_some<U>(self, signal: Optional<U, L, Bounded>) -> Stream<T, L, B, O, R> {
1179 self.filter_if(signal.is_some())
1180 }
1181
1182 /// Passes this stream through if the argument (a [`Bounded`] [`Optional`]) is null, otherwise the output is empty.
1183 ///
1184 /// Useful for gating the release of elements based on a condition, such as triggering a protocol if you are missing
1185 /// some local state.
1186 ///
1187 /// # Example
1188 /// ```rust
1189 /// # #[cfg(feature = "deploy")] {
1190 /// # use hydro_lang::prelude::*;
1191 /// # use futures::StreamExt;
1192 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1193 /// let tick = process.tick();
1194 /// // ticks are lazy by default, forces the second tick to run
1195 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1196 ///
1197 /// let batch_first_tick = process
1198 /// .source_iter(q!(vec![1, 2, 3, 4]))
1199 /// .batch(&tick, nondet!(/** test */));
1200 /// let batch_second_tick = process
1201 /// .source_iter(q!(vec![5, 6, 7, 8]))
1202 /// .batch(&tick, nondet!(/** test */))
1203 /// .defer_tick(); // appears on the second tick
1204 /// let some_on_first_tick = tick.optional_first_tick(q!(()));
1205 /// batch_first_tick.chain(batch_second_tick)
1206 /// .filter_if_none(some_on_first_tick)
1207 /// .all_ticks()
1208 /// # }, |mut stream| async move {
1209 /// // [5, 6, 7, 8]
1210 /// # for w in vec![5, 6, 7, 8] {
1211 /// # assert_eq!(stream.next().await.unwrap(), w);
1212 /// # }
1213 /// # }));
1214 /// # }
1215 /// ```
1216 #[deprecated(note = "use `filter_if` with `!Optional::is_some()` instead")]
1217 pub fn filter_if_none<U>(self, other: Optional<U, L, Bounded>) -> Stream<T, L, B, O, R> {
1218 self.filter_if(other.is_none())
1219 }
1220
1221 /// Forms the cross-product (Cartesian product, cross-join) of the items in the 2 input streams,
1222 /// returning all tupled pairs.
1223 ///
1224 /// When the right side is [`Bounded`], it is accumulated first and the left side streams
1225 /// through, preserving the left side's ordering. When both sides are [`Unbounded`], a
1226 /// symmetric hash join is used and ordering is [`NoOrder`].
1227 ///
1228 /// # Example
1229 /// ```rust
1230 /// # #[cfg(feature = "deploy")] {
1231 /// # use hydro_lang::prelude::*;
1232 /// # use std::collections::HashSet;
1233 /// # use futures::StreamExt;
1234 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1235 /// let tick = process.tick();
1236 /// let stream1 = process.source_iter(q!(vec![1, 2]));
1237 /// let stream2 = process.source_iter(q!(vec!['a', 'b']));
1238 /// stream1.cross_product(stream2)
1239 /// # }, |mut stream| async move {
1240 /// // (1, 'a'), (1, 'b'), (2, 'a'), (2, 'b') in any order
1241 /// # let expected = HashSet::from([(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]);
1242 /// # stream.map(|i| assert!(expected.contains(&i)));
1243 /// # }));
1244 /// # }
1245 pub fn cross_product<T2, B2: Boundedness, O2: Ordering, R2: Retries>(
1246 self,
1247 other: Stream<T2, L, B2, O2, R2>,
1248 ) -> Stream<(T, T2), L, B, B2::PreserveOrderIfBounded<O>, <R as MinRetries<R2>>::Min>
1249 where
1250 T: Clone,
1251 T2: Clone,
1252 R: MinRetries<R2>,
1253 {
1254 self.map(q!(|v| ((), v)))
1255 .join(other.map(q!(|v| ((), v))))
1256 .map(q!(|((), (v1, v2))| (v1, v2)))
1257 }
1258
1259 /// Takes one stream as input and filters out any duplicate occurrences. The output
1260 /// contains all unique values from the input.
1261 ///
1262 /// # Example
1263 /// ```rust
1264 /// # #[cfg(feature = "deploy")] {
1265 /// # use hydro_lang::prelude::*;
1266 /// # use futures::StreamExt;
1267 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1268 /// let tick = process.tick();
1269 /// process.source_iter(q!(vec![1, 2, 3, 2, 1, 4])).unique()
1270 /// # }, |mut stream| async move {
1271 /// # for w in vec![1, 2, 3, 4] {
1272 /// # assert_eq!(stream.next().await.unwrap(), w);
1273 /// # }
1274 /// # }));
1275 /// # }
1276 /// ```
1277 pub fn unique(self) -> Stream<T, L, B, O, ExactlyOnce>
1278 where
1279 T: Eq + Hash,
1280 {
1281 Stream::new(
1282 self.location.clone(),
1283 HydroNode::Unique {
1284 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1285 metadata: self
1286 .location
1287 .new_node_metadata(Stream::<T, L, B, O, ExactlyOnce>::collection_kind()),
1288 },
1289 )
1290 }
1291
1292 /// Outputs everything in this stream that is *not* contained in the `other` stream.
1293 ///
1294 /// The `other` stream must be [`Bounded`], since this function will wait until
1295 /// all its elements are available before producing any output.
1296 /// # Example
1297 /// ```rust
1298 /// # #[cfg(feature = "deploy")] {
1299 /// # use hydro_lang::prelude::*;
1300 /// # use futures::StreamExt;
1301 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1302 /// let tick = process.tick();
1303 /// let stream = process
1304 /// .source_iter(q!(vec![ 1, 2, 3, 4 ]))
1305 /// .batch(&tick, nondet!(/** test */));
1306 /// let batch = process
1307 /// .source_iter(q!(vec![1, 2]))
1308 /// .batch(&tick, nondet!(/** test */));
1309 /// stream.filter_not_in(batch).all_ticks()
1310 /// # }, |mut stream| async move {
1311 /// # for w in vec![3, 4] {
1312 /// # assert_eq!(stream.next().await.unwrap(), w);
1313 /// # }
1314 /// # }));
1315 /// # }
1316 /// ```
1317 pub fn filter_not_in<O2: Ordering, B2>(self, other: Stream<T, L, B2, O2, R>) -> Self
1318 where
1319 T: Eq + Hash,
1320 B2: IsBounded,
1321 {
1322 check_matching_location(&self.location, &other.location);
1323
1324 Stream::new(
1325 self.location.clone(),
1326 HydroNode::Difference {
1327 pos: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1328 neg: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
1329 metadata: self
1330 .location
1331 .new_node_metadata(Stream::<T, L, Bounded, O, R>::collection_kind()),
1332 },
1333 )
1334 }
1335
1336 /// An operator which allows you to "inspect" each element of a stream without
1337 /// modifying it. The closure `f` is called on a reference to each item. This is
1338 /// mainly useful for debugging, and should not be used to generate side-effects.
1339 ///
1340 /// If the input stream is unordered **and** `f` mutably captures state, `f` must be
1341 /// proven **commutative**: executing it on any two elements in either order must
1342 /// leave the mutably-captured state in the same final value. (The elements
1343 /// themselves pass through unchanged.) Similarly, if the input stream has retries
1344 /// (`AtLeastOnce`), `f` must be proven **idempotent**: executing it on an element
1345 /// twice in a row must leave the mutably-captured state in the same final value as
1346 /// executing it once.
1347 ///
1348 /// # Example
1349 /// ```rust
1350 /// # #[cfg(feature = "deploy")] {
1351 /// # use hydro_lang::prelude::*;
1352 /// # use futures::StreamExt;
1353 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1354 /// let nums = process.source_iter(q!(vec![1, 2]));
1355 /// // prints "1 * 10 = 10" and "2 * 10 = 20"
1356 /// nums.inspect(q!(|x| println!("{} * 10 = {}", x, x * 10)))
1357 /// # }, |mut stream| async move {
1358 /// # for w in vec![1, 2] {
1359 /// # assert_eq!(stream.next().await.unwrap(), w);
1360 /// # }
1361 /// # }));
1362 /// # }
1363 /// ```
1364 pub fn inspect<F, C, Idemp, const WAS_MUT: bool>(
1365 self,
1366 f: impl IntoQuotedMut<
1367 'a,
1368 F,
1369 OperatorContext<L::DropConsistency, B>,
1370 StreamMapFuncAlgebra<T, B, C, Idemp, L::SimHookScope>,
1371 >,
1372 ) -> Self
1373 where
1374 F: FnMut(&T) + 'a,
1375 C: ValidMutBorrowCommutativityFor<F, T, (), O, WAS_MUT>,
1376 Idemp: ValidMutBorrowIdempotenceFor<F, T, (), R, WAS_MUT>,
1377 {
1378 let f = crate::handoff_ref::with_ref_capture(|| {
1379 let (expr, proof) =
1380 f.splice_fnmut1_borrow_ctx_props(&OperatorContext::<L::DropConsistency, B>::new(
1381 &self.location.drop_consistency(),
1382 ));
1383 proof.register_proof(&expr);
1384 expr.into()
1385 });
1386
1387 Stream::new(
1388 self.location.clone(),
1389 HydroNode::Inspect {
1390 f,
1391 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1392 metadata: self.location.new_node_metadata(Self::collection_kind()),
1393 },
1394 )
1395 }
1396
1397 /// Executes the provided closure for every element in this stream.
1398 ///
1399 /// If the stream is unordered or has retries, the closure must demonstrate commutativity
1400 /// and/or idempotence via annotations:
1401 /// ```rust,ignore
1402 /// stream.for_each(q!(
1403 /// |x| *flag_mut |= x,
1404 /// commutative = manual_proof!(/** boolean OR is commutative */),
1405 /// idempotent = manual_proof!(/** boolean OR is idempotent */)
1406 /// ));
1407 /// ```
1408 ///
1409 /// **Commutative** here means that executing the closure on any two elements in
1410 /// either order must leave its side effects (e.g. mutably-captured state) in the
1411 /// same final value. **Idempotent** here means that executing the closure on an
1412 /// element twice in a row must leave its side effects in the same final value as
1413 /// executing it once.
1414 ///
1415 /// On a `TotalOrder + ExactlyOnce` stream, no annotations are needed.
1416 ///
1417 /// The closure may capture singletons via `by_ref()` or `by_mut()`, as long as the
1418 /// referenced collection lives at the same location and has the same boundedness as this
1419 /// stream.
1420 pub fn for_each<F: FnMut(T) + 'a, C, I>(
1421 self,
1422 f: impl IntoQuotedMut<
1423 'a,
1424 F,
1425 OperatorContext<L, B>,
1426 AggFuncAlgebra<T, B, C, I, NotProved, L::SimHookScope>,
1427 >,
1428 ) where
1429 C: ValidCommutativityFor<O>,
1430 I: ValidIdempotenceFor<R>,
1431 {
1432 let f = crate::handoff_ref::with_ref_capture(|| {
1433 let (f, proof) =
1434 f.splice_fnmut1_ctx_props(&OperatorContext::<L, B>::new(&self.location));
1435 proof.register_proof(&f);
1436 f.into()
1437 });
1438 self.location
1439 .flow_state()
1440 .borrow_mut()
1441 .push_root(HydroRoot::ForEach {
1442 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1443 f,
1444 op_metadata: HydroIrOpMetadata::new(),
1445 });
1446 }
1447
1448 /// Sends all elements of this stream to a provided [`futures::Sink`], such as an external
1449 /// TCP socket to some other server. You should _not_ use this API for interacting with
1450 /// external clients, instead see [`Location::bidi_external_many_bytes`] and
1451 /// [`Location::bidi_external_many_bincode`]. This should be used for custom, low-level
1452 /// interaction with asynchronous sinks.
1453 pub fn dest_sink<S>(self, sink: impl QuotedWithContext<'a, S, L>)
1454 where
1455 O: IsOrdered,
1456 R: IsExactlyOnce,
1457 S: 'a + futures::Sink<T> + Unpin,
1458 {
1459 self.location
1460 .flow_state()
1461 .borrow_mut()
1462 .push_root(HydroRoot::DestSink {
1463 sink: sink.splice_typed_ctx(&self.location).into(),
1464 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1465 op_metadata: HydroIrOpMetadata::new(),
1466 });
1467 }
1468
1469 /// Maps each element `x` of the stream to `(i, x)`, where `i` is the index of the element.
1470 ///
1471 /// # Example
1472 /// ```rust
1473 /// # #[cfg(feature = "deploy")] {
1474 /// # use hydro_lang::{prelude::*, live_collections::stream::{TotalOrder, ExactlyOnce}};
1475 /// # use futures::StreamExt;
1476 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test::<_, _, _, TotalOrder, ExactlyOnce>(|process| {
1477 /// let tick = process.tick();
1478 /// let numbers = process.source_iter(q!(vec![1, 2, 3, 4]));
1479 /// numbers.enumerate()
1480 /// # }, |mut stream| async move {
1481 /// // (0, 1), (1, 2), (2, 3), (3, 4)
1482 /// # for w in vec![(0, 1), (1, 2), (2, 3), (3, 4)] {
1483 /// # assert_eq!(stream.next().await.unwrap(), w);
1484 /// # }
1485 /// # }));
1486 /// # }
1487 /// ```
1488 pub fn enumerate(self) -> Stream<(usize, T), L, B, O, R>
1489 where
1490 O: IsOrdered,
1491 R: IsExactlyOnce,
1492 {
1493 Stream::new(
1494 self.location.clone(),
1495 HydroNode::Enumerate {
1496 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1497 metadata: self.location.new_node_metadata(Stream::<
1498 (usize, T),
1499 L,
1500 B,
1501 TotalOrder,
1502 ExactlyOnce,
1503 >::collection_kind()),
1504 },
1505 )
1506 }
1507
1508 /// Combines elements of the stream into a [`Singleton`], by starting with an intitial value,
1509 /// generated by the `init` closure, and then applying the `comb` closure to each element in the stream.
1510 /// Unlike iterators, `comb` takes the accumulator by `&mut` reference, so that it can be modified in place.
1511 ///
1512 /// Depending on the input stream guarantees, the closure may need to be commutative
1513 /// (for unordered streams) or idempotent (for streams with non-deterministic duplicates).
1514 ///
1515 /// # Example
1516 /// ```rust
1517 /// # #[cfg(feature = "deploy")] {
1518 /// # use hydro_lang::prelude::*;
1519 /// # use futures::StreamExt;
1520 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1521 /// let words = process.source_iter(q!(vec!["HELLO", "WORLD"]));
1522 /// words
1523 /// .fold(q!(|| String::new()), q!(|acc, x| acc.push_str(x)))
1524 /// .into_stream()
1525 /// # }, |mut stream| async move {
1526 /// // "HELLOWORLD"
1527 /// # assert_eq!(stream.next().await.unwrap(), "HELLOWORLD");
1528 /// # }));
1529 /// # }
1530 /// ```
1531 pub fn fold<A, I, F, C, Idemp, M, B2: SingletonBound>(
1532 self,
1533 init: impl IntoQuotedMut<'a, I, OperatorContext<L, B>>,
1534 comb: impl IntoQuotedMut<
1535 'a,
1536 F,
1537 OperatorContext<L, B>,
1538 AggFuncAlgebra<T, B, C, Idemp, M, L::SimHookScope>,
1539 >,
1540 ) -> Singleton<A, L, B2>
1541 where
1542 I: Fn() -> A + 'a,
1543 F: 'a + Fn(&mut A, T),
1544 C: ValidCommutativityFor<O>,
1545 Idemp: ValidIdempotenceFor<R>,
1546 B: ApplyMonotoneStream<M, B2>,
1547 {
1548 let init = init
1549 .splice_fn0_ctx(&OperatorContext::<L, B>::new(&self.location))
1550 .into();
1551 let (comb, proof) =
1552 comb.splice_fn2_borrow_mut_ctx_props(&OperatorContext::<L, B>::new(&self.location));
1553 let ordering_hook = proof.register_proof(&comb);
1554
1555 // Only assume_retries (for idempotence), not assume_ordering.
1556 // The fold hook in the simulator handles ordering non-determinism directly, so an
1557 // ordering hook on the commutativity proof binds to the fold operator itself.
1558 let nondet = nondet!(/** the combinator function is commutative and idempotent */);
1559 let retried: Stream<T, L::DropConsistency, B, O, ExactlyOnce> = self.assume_retries(nondet);
1560
1561 let mut metadata = retried
1562 .location
1563 .new_node_metadata(Singleton::<A, L::DropConsistency, B2>::collection_kind());
1564 metadata.op.sim_hook_id = ordering_hook.map(|hook| hook.id);
1565
1566 let core = HydroNode::Fold {
1567 init,
1568 acc: comb.into(),
1569 input: Box::new(retried.ir_node.replace(HydroNode::Placeholder)),
1570 metadata,
1571 // we do not guarantee consistency at this point because if the algebraic properties
1572 // do not hold in practice, replica consistency may fail to be maintained, so we
1573 // would like the simulator to assert consistency; in the future, this will be dynamic
1574 // based on the proof mechanism
1575 };
1576
1577 Singleton::new(retried.location.clone(), core)
1578 .assert_has_consistency_of(manual_proof!(/** algebraic properties */))
1579 }
1580
1581 /// Combines elements of the stream into an [`Optional`], by starting with the first element in the stream,
1582 /// and then applying the `comb` closure to each element in the stream. The [`Optional`] will be empty
1583 /// until the first element in the input arrives. Unlike iterators, `comb` takes the accumulator by `&mut`
1584 /// reference, so that it can be modified in place.
1585 ///
1586 /// Depending on the input stream guarantees, the closure may need to be commutative
1587 /// (for unordered streams) or idempotent (for streams with non-deterministic duplicates).
1588 ///
1589 /// # Example
1590 /// ```rust
1591 /// # #[cfg(feature = "deploy")] {
1592 /// # use hydro_lang::prelude::*;
1593 /// # use futures::StreamExt;
1594 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1595 /// let bools = process.source_iter(q!(vec![false, true, false]));
1596 /// bools.reduce(q!(|acc, x| *acc |= x)).into_stream()
1597 /// # }, |mut stream| async move {
1598 /// // true
1599 /// # assert_eq!(stream.next().await.unwrap(), true);
1600 /// # }));
1601 /// # }
1602 /// ```
1603 pub fn reduce<F, C, Idemp>(
1604 self,
1605 comb: impl IntoQuotedMut<
1606 'a,
1607 F,
1608 OperatorContext<L, B>,
1609 AggFuncAlgebra<T, B, C, Idemp, NotProved, L::SimHookScope>,
1610 >,
1611 ) -> Optional<T, L, B::AggregatedOptional>
1612 where
1613 F: Fn(&mut T, T) + 'a,
1614 C: ValidCommutativityFor<O>,
1615 Idemp: ValidIdempotenceFor<R>,
1616 {
1617 let (f, proof) =
1618 comb.splice_fn2_borrow_mut_ctx_props(&OperatorContext::<L, B>::new(&self.location));
1619 let ordering_hook = proof.register_proof(&f);
1620
1621 let nondet_retries = nondet!(/** the combinator function is commutative and idempotent */);
1622 let ordered_etc: Stream<T, L::DropConsistency, B> =
1623 self.assume_retries(nondet_retries).assume_ordering(nondet!(
1624 /// the combinator function is commutative; the simulator still explores
1625 /// (or scripts, via the proof's hook) the ordering
1626 hook = ordering_hook
1627 ));
1628
1629 let core = HydroNode::Reduce {
1630 f: f.into(),
1631 input: Box::new(ordered_etc.ir_node.replace(HydroNode::Placeholder)),
1632 metadata: ordered_etc.location.new_node_metadata(Optional::<
1633 T,
1634 L::DropConsistency,
1635 B::AggregatedOptional,
1636 >::collection_kind()),
1637 };
1638
1639 Optional::new(ordered_etc.location.clone(), core)
1640 .assert_has_consistency_of(manual_proof!(/** algebraic properties */))
1641 }
1642
1643 /// Computes the maximum element in the stream as an [`Optional`], which
1644 /// will be empty until the first element in the input arrives.
1645 ///
1646 /// # Example
1647 /// ```rust
1648 /// # #[cfg(feature = "deploy")] {
1649 /// # use hydro_lang::prelude::*;
1650 /// # use futures::StreamExt;
1651 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1652 /// let tick = process.tick();
1653 /// let numbers = process.source_iter(q!(vec![1, 2, 3, 4]));
1654 /// let batch = numbers.batch(&tick, nondet!(/** test */));
1655 /// batch.max().all_ticks()
1656 /// # }, |mut stream| async move {
1657 /// // 4
1658 /// # assert_eq!(stream.next().await.unwrap(), 4);
1659 /// # }));
1660 /// # }
1661 /// ```
1662 pub fn max(self) -> Optional<T, L, B::AggregatedOptional>
1663 where
1664 T: Ord,
1665 {
1666 self.assume_retries_trusted::<ExactlyOnce>(nondet!(/** max is idempotent */))
1667 .assume_ordering_trusted_bounded::<TotalOrder>(
1668 nondet!(/** max is commutative, but order affects intermediates */),
1669 )
1670 .reduce(q!(|curr, new| {
1671 if new > *curr {
1672 *curr = new;
1673 }
1674 }))
1675 }
1676
1677 /// Computes the minimum element in the stream as an [`Optional`], which
1678 /// will be empty until the first element in the input arrives.
1679 ///
1680 /// # Example
1681 /// ```rust
1682 /// # #[cfg(feature = "deploy")] {
1683 /// # use hydro_lang::prelude::*;
1684 /// # use futures::StreamExt;
1685 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1686 /// let tick = process.tick();
1687 /// let numbers = process.source_iter(q!(vec![1, 2, 3, 4]));
1688 /// let batch = numbers.batch(&tick, nondet!(/** test */));
1689 /// batch.min().all_ticks()
1690 /// # }, |mut stream| async move {
1691 /// // 1
1692 /// # assert_eq!(stream.next().await.unwrap(), 1);
1693 /// # }));
1694 /// # }
1695 /// ```
1696 pub fn min(self) -> Optional<T, L, B::AggregatedOptional>
1697 where
1698 T: Ord,
1699 {
1700 self.assume_retries_trusted::<ExactlyOnce>(nondet!(/** min is idempotent */))
1701 .assume_ordering_trusted_bounded::<TotalOrder>(
1702 nondet!(/** max is commutative, but order affects intermediates */),
1703 )
1704 .reduce(q!(|curr, new| {
1705 if new < *curr {
1706 *curr = new;
1707 }
1708 }))
1709 }
1710
1711 /// Computes the first element in the stream as an [`Optional`], which
1712 /// will be empty until the first element in the input arrives.
1713 ///
1714 /// This requires the stream to have a [`TotalOrder`] guarantee, otherwise
1715 /// re-ordering of elements may cause the first element to change.
1716 ///
1717 /// # Example
1718 /// ```rust
1719 /// # #[cfg(feature = "deploy")] {
1720 /// # use hydro_lang::prelude::*;
1721 /// # use futures::StreamExt;
1722 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1723 /// let tick = process.tick();
1724 /// let numbers = process.source_iter(q!(vec![1, 2, 3, 4]));
1725 /// let batch = numbers.batch(&tick, nondet!(/** test */));
1726 /// batch.first().all_ticks()
1727 /// # }, |mut stream| async move {
1728 /// // 1
1729 /// # assert_eq!(stream.next().await.unwrap(), 1);
1730 /// # }));
1731 /// # }
1732 /// ```
1733 pub fn first(self) -> Optional<T, L, B::AggregatedOptional>
1734 where
1735 O: IsOrdered,
1736 {
1737 self.make_totally_ordered()
1738 .assume_retries_trusted::<ExactlyOnce>(nondet!(/** first is idempotent */))
1739 .generator(q!(|| ()), q!(|_, item| Generate::Return(item)))
1740 .reduce(q!(|_, _| {}))
1741 }
1742
1743 /// Computes the last element in the stream as an [`Optional`], which
1744 /// will be empty until an element in the input arrives.
1745 ///
1746 /// This requires the stream to have a [`TotalOrder`] guarantee, otherwise
1747 /// re-ordering of elements may cause the last element to change.
1748 ///
1749 /// # Example
1750 /// ```rust
1751 /// # #[cfg(feature = "deploy")] {
1752 /// # use hydro_lang::prelude::*;
1753 /// # use futures::StreamExt;
1754 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1755 /// let tick = process.tick();
1756 /// let numbers = process.source_iter(q!(vec![1, 2, 3, 4]));
1757 /// let batch = numbers.batch(&tick, nondet!(/** test */));
1758 /// batch.last().all_ticks()
1759 /// # }, |mut stream| async move {
1760 /// // 4
1761 /// # assert_eq!(stream.next().await.unwrap(), 4);
1762 /// # }));
1763 /// # }
1764 /// ```
1765 pub fn last(self) -> Optional<T, L, B::AggregatedOptional>
1766 where
1767 O: IsOrdered,
1768 {
1769 self.make_totally_ordered()
1770 .assume_retries_trusted::<ExactlyOnce>(nondet!(/** last is idempotent */))
1771 .reduce(q!(|curr, new| *curr = new))
1772 }
1773
1774 /// Returns a stream containing at most the first `n` elements of the input stream,
1775 /// preserving the original order. Similar to `LIMIT` in SQL.
1776 ///
1777 /// This requires the stream to have a [`TotalOrder`] guarantee and [`ExactlyOnce`]
1778 /// retries, since the result depends on the order and cardinality of elements.
1779 ///
1780 /// # Example
1781 /// ```rust
1782 /// # #[cfg(feature = "deploy")] {
1783 /// # use hydro_lang::prelude::*;
1784 /// # use futures::StreamExt;
1785 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1786 /// let numbers = process.source_iter(q!(vec![10, 20, 30, 40, 50]));
1787 /// numbers.limit(q!(3))
1788 /// # }, |mut stream| async move {
1789 /// // 10, 20, 30
1790 /// # for w in vec![10, 20, 30] {
1791 /// # assert_eq!(stream.next().await.unwrap(), w);
1792 /// # }
1793 /// # }));
1794 /// # }
1795 /// ```
1796 pub fn limit(
1797 self,
1798 n: impl QuotedWithContext<'a, usize, OperatorContext<L, B>> + Copy + 'a,
1799 ) -> Stream<T, L, B, TotalOrder, ExactlyOnce>
1800 where
1801 O: IsOrdered,
1802 R: IsExactlyOnce,
1803 {
1804 self.generator(
1805 q!(|| 0usize),
1806 q!(move |count, item| {
1807 if *count == n {
1808 Generate::Break
1809 } else {
1810 *count += 1;
1811 if *count == n {
1812 Generate::Return(item)
1813 } else {
1814 Generate::Yield(item)
1815 }
1816 }
1817 }),
1818 )
1819 }
1820
1821 /// Collects all the elements of this stream into a single [`Vec`] element.
1822 ///
1823 /// If the input stream is [`Unbounded`], the output [`Singleton`] will be [`Unbounded`] as
1824 /// well, which means that the value of the [`Vec`] will asynchronously grow as new elements
1825 /// are added. On such a value, you can use [`Singleton::snapshot`] to grab an instance of
1826 /// the vector at an arbitrary point in time.
1827 ///
1828 /// # Example
1829 /// ```rust
1830 /// # #[cfg(feature = "deploy")] {
1831 /// # use hydro_lang::prelude::*;
1832 /// # use futures::StreamExt;
1833 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1834 /// let tick = process.tick();
1835 /// let numbers = process.source_iter(q!(vec![1, 2, 3, 4]));
1836 /// let batch = numbers.batch(&tick, nondet!(/** test */));
1837 /// batch.collect_vec().all_ticks() // emit each tick's Vec into an unbounded stream
1838 /// # }, |mut stream| async move {
1839 /// // [ vec![1, 2, 3, 4] ]
1840 /// # for w in vec![vec![1, 2, 3, 4]] {
1841 /// # assert_eq!(stream.next().await.unwrap(), w);
1842 /// # }
1843 /// # }));
1844 /// # }
1845 /// ```
1846 pub fn collect_vec(self) -> Singleton<Vec<T>, L, B>
1847 where
1848 O: IsOrdered,
1849 R: IsExactlyOnce,
1850 {
1851 self.make_totally_ordered().make_exactly_once().fold(
1852 q!(|| vec![]),
1853 q!(|acc, v| {
1854 acc.push(v);
1855 }),
1856 )
1857 }
1858
1859 /// Applies a function to each element of the stream, maintaining an internal state (accumulator)
1860 /// and emitting each intermediate result.
1861 ///
1862 /// Unlike `fold` which only returns the final accumulated value, `scan` produces a new stream
1863 /// containing all intermediate accumulated values. The scan operation can also terminate early
1864 /// by returning `None`.
1865 ///
1866 /// The function takes a mutable reference to the accumulator and the current element, and returns
1867 /// an `Option<U>`. If the function returns `Some(value)`, `value` is emitted to the output stream.
1868 /// If the function returns `None`, the stream is terminated and no more elements are processed.
1869 ///
1870 /// The `init` and `f` closures may capture bounded singletons, optionals, or streams by
1871 /// reference via [`by_ref()`](crate::live_collections::Singleton::by_ref), as long as the
1872 /// referenced collection lives at the same location and has the same boundedness as this
1873 /// stream.
1874 ///
1875 /// # Examples
1876 ///
1877 /// Basic usage - running sum:
1878 /// ```rust
1879 /// # #[cfg(feature = "deploy")] {
1880 /// # use hydro_lang::prelude::*;
1881 /// # use futures::StreamExt;
1882 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1883 /// process.source_iter(q!(vec![1, 2, 3, 4])).scan(
1884 /// q!(|| 0),
1885 /// q!(|acc, x| {
1886 /// *acc += x;
1887 /// Some(*acc)
1888 /// }),
1889 /// )
1890 /// # }, |mut stream| async move {
1891 /// // Output: 1, 3, 6, 10
1892 /// # for w in vec![1, 3, 6, 10] {
1893 /// # assert_eq!(stream.next().await.unwrap(), w);
1894 /// # }
1895 /// # }));
1896 /// # }
1897 /// ```
1898 ///
1899 /// Early termination example:
1900 /// ```rust
1901 /// # #[cfg(feature = "deploy")] {
1902 /// # use hydro_lang::prelude::*;
1903 /// # use futures::StreamExt;
1904 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1905 /// process.source_iter(q!(vec![1, 2, 3, 4])).scan(
1906 /// q!(|| 1),
1907 /// q!(|state, x| {
1908 /// *state = *state * x;
1909 /// if *state > 6 {
1910 /// None // Terminate the stream
1911 /// } else {
1912 /// Some(-*state)
1913 /// }
1914 /// }),
1915 /// )
1916 /// # }, |mut stream| async move {
1917 /// // Output: -1, -2, -6
1918 /// # for w in vec![-1, -2, -6] {
1919 /// # assert_eq!(stream.next().await.unwrap(), w);
1920 /// # }
1921 /// # }));
1922 /// # }
1923 /// ```
1924 pub fn scan<A, U, I, F>(
1925 self,
1926 init: impl IntoQuotedMut<'a, I, OperatorContext<L, B>>,
1927 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>>,
1928 ) -> Stream<U, L, B, TotalOrder, ExactlyOnce>
1929 where
1930 O: IsOrdered,
1931 R: IsExactlyOnce,
1932 I: Fn() -> A + 'a,
1933 F: Fn(&mut A, T) -> Option<U> + 'a,
1934 {
1935 let init = crate::handoff_ref::with_ref_capture(|| {
1936 init.splice_fn0_ctx(&OperatorContext::<L, B>::new(&self.location))
1937 .into()
1938 });
1939 let f = crate::handoff_ref::with_ref_capture(|| {
1940 f.splice_fn2_borrow_mut_ctx(&OperatorContext::<L, B>::new(&self.location))
1941 .into()
1942 });
1943
1944 Stream::new(
1945 self.location.clone(),
1946 HydroNode::Scan {
1947 init,
1948 acc: f,
1949 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1950 metadata: self.location.new_node_metadata(
1951 Stream::<U, L, B, TotalOrder, ExactlyOnce>::collection_kind(),
1952 ),
1953 },
1954 )
1955 }
1956
1957 /// Async version of [`Stream::scan`]. Applies an async function to each element of the
1958 /// stream, maintaining an internal state (accumulator) and emitting the values returned
1959 /// by the function.
1960 ///
1961 /// The closure runs synchronously (so it can mutate the accumulator), then returns a
1962 /// future. The future is polled to completion. If it resolves to `Some`, the value is
1963 /// emitted. If it resolves to `None`, the item is filtered out.
1964 ///
1965 /// The `init` and `f` closures may capture bounded singletons, optionals, or streams by
1966 /// reference via [`by_ref()`](crate::live_collections::Singleton::by_ref), as long as the
1967 /// referenced collection lives at the same location and has the same boundedness as this
1968 /// stream.
1969 ///
1970 /// # Examples
1971 ///
1972 /// ```rust
1973 /// # #[cfg(feature = "deploy")] {
1974 /// # use hydro_lang::prelude::*;
1975 /// # use futures::StreamExt;
1976 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1977 /// process
1978 /// .source_iter(q!(vec![1, 2, 3, 4]))
1979 /// .scan_async_blocking(
1980 /// q!(|| 0),
1981 /// q!(|acc, x| {
1982 /// *acc += x;
1983 /// let val = *acc;
1984 /// async move { Some(val) }
1985 /// }),
1986 /// )
1987 /// # }, |mut stream| async move {
1988 /// // Output: 1, 3, 6, 10
1989 /// # for w in vec![1, 3, 6, 10] {
1990 /// # assert_eq!(stream.next().await.unwrap(), w);
1991 /// # }
1992 /// # }));
1993 /// # }
1994 /// ```
1995 pub fn scan_async_blocking<A, U, I, F, Fut>(
1996 self,
1997 init: impl IntoQuotedMut<'a, I, OperatorContext<L, B>>,
1998 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>>,
1999 ) -> Stream<U, L, B, TotalOrder, ExactlyOnce>
2000 where
2001 O: IsOrdered,
2002 R: IsExactlyOnce,
2003 I: Fn() -> A + 'a,
2004 F: Fn(&mut A, T) -> Fut + 'a,
2005 Fut: Future<Output = Option<U>> + 'a,
2006 {
2007 let init = crate::handoff_ref::with_ref_capture(|| {
2008 init.splice_fn0_ctx(&OperatorContext::<L, B>::new(&self.location))
2009 .into()
2010 });
2011 let f = crate::handoff_ref::with_ref_capture(|| {
2012 f.splice_fn2_borrow_mut_ctx(&OperatorContext::<L, B>::new(&self.location))
2013 .into()
2014 });
2015
2016 Stream::new(
2017 self.location.clone(),
2018 HydroNode::ScanAsyncBlocking {
2019 init,
2020 acc: f,
2021 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2022 metadata: self.location.new_node_metadata(
2023 Stream::<U, L, B, TotalOrder, ExactlyOnce>::collection_kind(),
2024 ),
2025 },
2026 )
2027 }
2028
2029 /// Iteratively processes the elements of the stream using a state machine that can yield
2030 /// elements as it processes its inputs. This is designed to mirror the unstable generator
2031 /// syntax in Rust, without requiring special syntax.
2032 ///
2033 /// Like [`Stream::scan`], this function takes in an initializer that emits the initial
2034 /// state. The second argument defines the processing logic, taking in a mutable reference
2035 /// to the state and the value to be processed. It emits a [`Generate`] value, whose
2036 /// variants define what is emitted and whether further inputs should be processed.
2037 ///
2038 /// The `init` and `f` closures may capture bounded singletons, optionals, or streams by
2039 /// reference via [`by_ref()`](crate::live_collections::Singleton::by_ref), as long as the
2040 /// referenced collection lives at the same location and has the same boundedness as this
2041 /// stream.
2042 ///
2043 /// # Example
2044 /// ```rust
2045 /// # #[cfg(feature = "deploy")] {
2046 /// # use hydro_lang::prelude::*;
2047 /// # use futures::StreamExt;
2048 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2049 /// process.source_iter(q!(vec![1, 3, 100, 10])).generator(
2050 /// q!(|| 0),
2051 /// q!(|acc, x| {
2052 /// *acc += x;
2053 /// if *acc > 100 {
2054 /// hydro_lang::live_collections::keyed_stream::Generate::Return("done!".to_owned())
2055 /// } else if *acc % 2 == 0 {
2056 /// hydro_lang::live_collections::keyed_stream::Generate::Yield("even".to_owned())
2057 /// } else {
2058 /// hydro_lang::live_collections::keyed_stream::Generate::Continue
2059 /// }
2060 /// }),
2061 /// )
2062 /// # }, |mut stream| async move {
2063 /// // Output: "even", "done!"
2064 /// # let mut results = Vec::new();
2065 /// # for _ in 0..2 {
2066 /// # results.push(stream.next().await.unwrap());
2067 /// # }
2068 /// # results.sort();
2069 /// # assert_eq!(results, vec!["done!".to_owned(), "even".to_owned()]);
2070 /// # }));
2071 /// # }
2072 /// ```
2073 pub fn generator<A, U, I, F>(
2074 self,
2075 init: impl IntoQuotedMut<'a, I, OperatorContext<L, B>> + Copy,
2076 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>> + Copy,
2077 ) -> Stream<U, L, B, TotalOrder, ExactlyOnce>
2078 where
2079 O: IsOrdered,
2080 R: IsExactlyOnce,
2081 I: Fn() -> A + 'a,
2082 F: Fn(&mut A, T) -> Generate<U> + 'a,
2083 {
2084 let init: ManualExpr<I, _> =
2085 ManualExpr::new(move |ctx: &OperatorContext<L, B>| init.splice_fn0_ctx(ctx));
2086 let f: ManualExpr<F, _> =
2087 ManualExpr::new(move |ctx: &OperatorContext<L, B>| f.splice_fn2_borrow_mut_ctx(ctx));
2088
2089 let this = self.make_totally_ordered().make_exactly_once();
2090
2091 // State is Option<Option<A>>:
2092 // None = not yet initialized
2093 // Some(Some(a)) = active with state a
2094 // Some(None) = terminated
2095 let scan_init = crate::handoff_ref::with_ref_capture(|| {
2096 q!(|| None)
2097 .splice_fn0_ctx::<Option<Option<A>>>(&this.location)
2098 .into()
2099 });
2100 let scan_f = crate::handoff_ref::with_ref_capture(|| {
2101 q!(move |state: &mut Option<Option<_>>, v| {
2102 if state.is_none() {
2103 *state = Some(Some(init()));
2104 }
2105 match state {
2106 Some(Some(state_value)) => match f(state_value, v) {
2107 Generate::Yield(out) => Some(Some(out)),
2108 Generate::Return(out) => {
2109 *state = Some(None);
2110 Some(Some(out))
2111 }
2112 // Unlike KeyedStream, we can terminate the scan directly on
2113 // Break/Return because there is only one state (no other keys
2114 // that still need processing).
2115 Generate::Break => None,
2116 Generate::Continue => Some(None),
2117 },
2118 // State is Some(None) after Return; terminate the scan.
2119 _ => None,
2120 }
2121 })
2122 .splice_fn2_borrow_mut_ctx::<Option<Option<A>>, T, _>(&OperatorContext::<L, B>::new(
2123 &this.location,
2124 ))
2125 .into()
2126 });
2127
2128 let scan_node = HydroNode::Scan {
2129 init: scan_init,
2130 acc: scan_f,
2131 input: Box::new(this.ir_node.replace(HydroNode::Placeholder)),
2132 metadata: this.location.new_node_metadata(Stream::<
2133 Option<U>,
2134 L,
2135 B,
2136 TotalOrder,
2137 ExactlyOnce,
2138 >::collection_kind()),
2139 };
2140
2141 let flatten_f = q!(|d| d)
2142 .splice_fn1_ctx::<Option<U>, _>(&this.location)
2143 .into();
2144 let flatten_node = HydroNode::FlatMap {
2145 f: flatten_f,
2146 input: Box::new(scan_node),
2147 metadata: this
2148 .location
2149 .new_node_metadata(Stream::<U, L, B, TotalOrder, ExactlyOnce>::collection_kind()),
2150 };
2151
2152 Stream::new(this.location.clone(), flatten_node)
2153 }
2154
2155 /// Given a time interval, returns a stream corresponding to samples taken from the
2156 /// stream roughly at that interval. The output will have elements in the same order
2157 /// as the input, but with arbitrary elements skipped between samples. There is also
2158 /// no guarantee on the exact timing of the samples.
2159 ///
2160 /// # Non-Determinism
2161 /// The output stream is non-deterministic in which elements are sampled, since this
2162 /// is controlled by a clock.
2163 ///
2164 /// In simulation tests, the internal batching of elements and of clock samples can be
2165 /// scripted through the guard's composite hook payload, e.g.
2166 /// `nondet!(/** reason */ hook = (elements_hook.into(), None))`.
2167 #[cfg(feature = "tokio")]
2168 #[expect(
2169 clippy::type_complexity,
2170 reason = "composite hook payload names each internal operator's handle type"
2171 )]
2172 pub fn sample_every(
2173 self,
2174 interval: impl QuotedWithContext<'a, std::time::Duration, L> + Copy + 'a,
2175 mut nondet: NonDet<(
2176 Option<crate::sim_hooks::BatchHook<T, O, R, L::SimHookScope>>,
2177 Option<crate::sim_hooks::BatchHook<(), TotalOrder, AtLeastOnce, L::SimHookScope>>,
2178 )>,
2179 ) -> Stream<T, L::DropConsistency, Unbounded, O, AtLeastOnce>
2180 where
2181 L: TopLevel<'a>,
2182 {
2183 let samples = self.location.source_interval(interval);
2184 let (elements_hook, samples_hook) = nondet.take_hook();
2185
2186 let tick = self.location.tick();
2187 self.batch(
2188 &tick,
2189 nondet!(
2190 /// which elements are batched between samples is captured by the caller's guard
2191 hook = elements_hook
2192 ),
2193 )
2194 .filter_if(
2195 samples
2196 .batch(
2197 &tick,
2198 nondet!(
2199 /// sample timing is captured by the caller's guard
2200 hook = samples_hook
2201 ),
2202 )
2203 .first()
2204 .is_some(),
2205 )
2206 .all_ticks()
2207 .weaken_retries()
2208 }
2209
2210 /// Given a timeout duration, returns an [`Optional`] which will have a value if the
2211 /// stream has not emitted a value since that duration.
2212 ///
2213 /// The clock is read once per tick (`use::time`); the time of the latest arrival is a
2214 /// `use::state` register stamped with that reading. No wall-clock read hides inside a
2215 /// closure: the verdict at a tick is a function of the tick's inputs and its time.
2216 ///
2217 /// # Non-Determinism
2218 /// Timeout relies on non-deterministic sampling of the stream, so depending on when
2219 /// samples take place, timeouts may be non-deterministically generated or missed,
2220 /// and the notification of the timeout may be delayed as well. There is also no
2221 /// guarantee on how long the [`Optional`] will have a value after the timeout is
2222 /// detected based on when the next sample is taken.
2223 #[cfg(feature = "tokio")]
2224 pub fn timeout(
2225 self,
2226 duration: impl QuotedWithContext<
2227 'a,
2228 std::time::Duration,
2229 OperatorContext<Tick<L::DropConsistency>, Bounded>,
2230 > + Copy
2231 + 'a,
2232 nondet: NonDet,
2233 ) -> Optional<(), L::DropConsistency, Unbounded>
2234 where
2235 L: TopLevel<'a>,
2236 {
2237 sliced! {
2238 let new_arrivals = use::batch(self, nondet!(
2239 /// sampling timing is captured by the caller's guard
2240 nondet
2241 ));
2242 let now = use::time(nondet!(
2243 /// the verdict depends on when the tick runs; captured by the caller's guard
2244 nondet
2245 ));
2246 let mut latest_received = use::state_null::<Optional<tokio::time::Instant, Tick<_>, Bounded>>();
2247
2248 // the latest arrival is stamped with this tick's clock reading
2249 latest_received = now
2250 .clone()
2251 .filter_if(new_arrivals.is_empty().map(q!(|empty| !empty)))
2252 .or(latest_received);
2253
2254 latest_received
2255 .clone()
2256 .into_singleton()
2257 .zip(now)
2258 .filter_map(q!(move |(latest_received, now)| {
2259 if let Some(latest_received) = latest_received {
2260 if now.duration_since(latest_received) > duration {
2261 Some(())
2262 } else {
2263 None
2264 }
2265 } else {
2266 Some(())
2267 }
2268 }))
2269 }
2270 }
2271
2272 /// Shifts this stream into an atomic context, which guarantees that any downstream logic
2273 /// will all be executed synchronously before any outputs are yielded (in [`Stream::end_atomic`]).
2274 ///
2275 /// This is useful to enforce local consistency constraints, such as ensuring that a write is
2276 /// processed before an acknowledgement is emitted.
2277 pub fn atomic(self) -> Stream<T, Atomic<L>, B, O, R>
2278 where
2279 L: TopLevel<'a>,
2280 {
2281 let out_location = Atomic {
2282 tick: self.location.tick(),
2283 };
2284 Stream::new(
2285 out_location.clone(),
2286 HydroNode::BeginAtomic {
2287 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2288 metadata: out_location
2289 .new_node_metadata(Stream::<T, Atomic<L>, B, O, R>::collection_kind()),
2290 },
2291 )
2292 }
2293
2294 /// Given a tick, returns a stream corresponding to a batch of elements segmented by
2295 /// that tick. These batches are guaranteed to be contiguous across ticks and preserve
2296 /// the order of the input. The output stream will execute in the [`Tick`] that was
2297 /// used to create the atomic section.
2298 ///
2299 /// # Non-Determinism
2300 /// The batch boundaries are non-deterministic and may change across executions.
2301 ///
2302 /// In simulation tests, the batching decisions can be scripted by attaching a
2303 /// [`BatchHook`](crate::sim_hooks::BatchHook) to the guard via
2304 /// `nondet!(/** reason */ hook = my_hook)`.
2305 pub fn batch<L2: Location<'a, DropConsistency = L::DropConsistency>>(
2306 self,
2307 tick: &Tick<L2>,
2308 mut nondet: NonDet<Option<crate::sim_hooks::BatchHook<T, O, R, L::SimHookScope>>>,
2309 ) -> Stream<T, Tick<L::DropConsistency>, Bounded, O, R> {
2310 assert_eq!(
2311 Location::id(tick.parent_location()),
2312 Location::id(&self.location)
2313 );
2314
2315 let mut metadata =
2316 tick.new_node_metadata(Stream::<T, Tick<L>, Bounded, O, R>::collection_kind());
2317 metadata.op.sim_hook_id = nondet.take_hook().map(|h| h.id);
2318 Stream::new(
2319 tick.drop_consistency(),
2320 HydroNode::Batch {
2321 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2322 metadata,
2323 },
2324 )
2325 }
2326
2327 /// An operator which allows you to "name" a `HydroNode`.
2328 /// This is only used for testing, to correlate certain `HydroNode`s with IDs.
2329 pub fn ir_node_named(self, name: &str) -> Stream<T, L, B, O, R> {
2330 {
2331 let mut node = self.ir_node.borrow_mut();
2332 let metadata = node.metadata_mut();
2333 metadata.tag = Some(name.to_owned());
2334 }
2335 self
2336 }
2337
2338 /// Turns this [`Stream`] into a [`Optional`], under the invariant assumption that there is at
2339 /// most one element. If this invariant is broken, the program may exhibit undefined behavior,
2340 /// so uses must be carefully vetted.
2341 pub(crate) fn cast_at_most_one_element(self) -> Optional<T, L, B>
2342 where
2343 B: IsBounded,
2344 {
2345 Optional::new(
2346 self.location.clone(),
2347 HydroNode::Cast {
2348 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2349 metadata: self
2350 .location
2351 .new_node_metadata(Optional::<T, L, B>::collection_kind()),
2352 },
2353 )
2354 }
2355
2356 pub(crate) fn use_ordering_type<O2: Ordering>(self) -> Stream<T, L, B, O2, R> {
2357 if O::ORDERING_KIND == O2::ORDERING_KIND {
2358 Stream::new(
2359 self.location.clone(),
2360 self.ir_node.replace(HydroNode::Placeholder),
2361 )
2362 } else {
2363 panic!(
2364 "Runtime ordering {:?} did not match requested cast {:?}.",
2365 O::ORDERING_KIND,
2366 O2::ORDERING_KIND
2367 )
2368 }
2369 }
2370
2371 /// Explicitly "casts" the stream to a type with a different ordering
2372 /// guarantee. Useful in unsafe code where the ordering cannot be proven
2373 /// by the type-system.
2374 ///
2375 /// # Non-Determinism
2376 /// This function is used as an escape hatch, and any mistakes in the
2377 /// provided ordering guarantee will propagate into the guarantees
2378 /// for the rest of the program.
2379 pub fn assume_ordering<O2: Ordering>(
2380 self,
2381 mut nondet: NonDet<Option<crate::sim_hooks::OrderingHook<T, B, R, L::SimHookScope>>>,
2382 ) -> Stream<T, L::DropConsistency, B, O2, R> {
2383 if O::ORDERING_KIND == O2::ORDERING_KIND {
2384 self.use_ordering_type().weaken_consistency()
2385 } else if O2::ORDERING_KIND == StreamOrder::NoOrder {
2386 // We can always weaken the ordering guarantee
2387 let target_location = self.location().drop_consistency();
2388 Stream::new(
2389 target_location.clone(),
2390 HydroNode::Cast {
2391 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2392 metadata: target_location
2393 .new_node_metadata(Stream::<T, L, B, O2, R>::collection_kind()),
2394 },
2395 )
2396 } else {
2397 let target_location = self.location().drop_consistency();
2398 let mut metadata =
2399 target_location.new_node_metadata(Stream::<T, L, B, O2, R>::collection_kind());
2400 metadata.op.sim_hook_id = nondet.take_hook().map(|hook| hook.id);
2401 Stream::new(
2402 target_location,
2403 HydroNode::ObserveNonDet {
2404 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2405 trusted: false,
2406 metadata,
2407 },
2408 )
2409 }
2410 }
2411
2412 // like `assume_ordering_trusted`, but only if the input stream is bounded and therefore
2413 // intermediate states will not be revealed
2414 fn assume_ordering_trusted_bounded<O2: Ordering>(
2415 self,
2416 nondet: NonDet,
2417 ) -> Stream<T, L, B, O2, R> {
2418 if B::BOUNDED {
2419 self.assume_ordering_trusted(nondet)
2420 } else {
2421 let self_location = self.location.clone();
2422 let inner: Stream<T, L::DropConsistency, B, O2, R> = self.assume_ordering(nondet!(
2423 /// the unbounded stream exposes ordering non-determinism in intermediate states
2424 nondet
2425 ));
2426 Stream::new(self_location, inner.ir_node.replace(HydroNode::Placeholder))
2427 }
2428 }
2429
2430 // only for internal APIs that have been carefully vetted to ensure that the non-determinism
2431 // is not observable
2432 pub(crate) fn assume_ordering_trusted<O2: Ordering>(
2433 self,
2434 _nondet: NonDet,
2435 ) -> Stream<T, L, B, O2, R> {
2436 if O::ORDERING_KIND == O2::ORDERING_KIND {
2437 self.use_ordering_type()
2438 } else if O2::ORDERING_KIND == StreamOrder::NoOrder {
2439 // We can always weaken the ordering guarantee
2440 Stream::new(
2441 self.location.clone(),
2442 HydroNode::Cast {
2443 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2444 metadata: self
2445 .location
2446 .new_node_metadata(Stream::<T, L, B, O2, R>::collection_kind()),
2447 },
2448 )
2449 } else {
2450 Stream::new(
2451 self.location.clone(),
2452 HydroNode::ObserveNonDet {
2453 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2454 trusted: true,
2455 metadata: self
2456 .location
2457 .new_node_metadata(Stream::<T, L, B, O2, R>::collection_kind()),
2458 },
2459 )
2460 }
2461 }
2462
2463 #[deprecated = "use `weaken_ordering::<NoOrder>()` instead"]
2464 /// Weakens the ordering guarantee provided by the stream to [`NoOrder`],
2465 /// which is always safe because that is the weakest possible guarantee.
2466 pub fn weakest_ordering(self) -> Stream<T, L, B, NoOrder, R> {
2467 self.weaken_ordering::<NoOrder>()
2468 }
2469
2470 /// Weakens the ordering guarantee provided by the stream to `O2`, with the type-system
2471 /// enforcing that `O2` is weaker than the input ordering guarantee.
2472 pub fn weaken_ordering<O2: WeakerOrderingThan<O>>(self) -> Stream<T, L, B, O2, R> {
2473 let nondet = nondet!(/** this is a weaker ordering guarantee, so it is safe to assume */);
2474 self.assume_ordering_trusted::<O2>(nondet)
2475 }
2476
2477 /// Strengthens the ordering guarantee to `TotalOrder`, given that `O: IsOrdered`, which
2478 /// implies that `O == TotalOrder`.
2479 pub fn make_totally_ordered(self) -> Stream<T, L, B, TotalOrder, R>
2480 where
2481 O: IsOrdered,
2482 {
2483 self.assume_ordering_trusted(nondet!(/** no-op */))
2484 }
2485
2486 /// Explicitly "casts" the stream to a type with a different retries
2487 /// guarantee. Useful in unsafe code where the lack of retries cannot
2488 /// be proven by the type-system.
2489 ///
2490 /// # Non-Determinism
2491 /// This function is used as an escape hatch, and any mistakes in the
2492 /// provided retries guarantee will propagate into the guarantees
2493 /// for the rest of the program.
2494 ///
2495 /// In simulation tests, the retry decisions (how many times each element is
2496 /// observed) can be scripted by attaching a
2497 /// [`RetriesHook`](crate::sim_hooks::RetriesHook) to the guard via
2498 /// `nondet!(/** reason */ hook = my_hook)`. Because every element admits arbitrarily
2499 /// many retries, this non-determinism cannot be explored autonomously: simulating a
2500 /// program containing a strengthening `assume_retries` **requires** a bound hook.
2501 pub fn assume_retries<R2: Retries>(
2502 self,
2503 mut nondet: NonDet<Option<crate::sim_hooks::RetriesHook<T, O, B, L::SimHookScope>>>,
2504 ) -> Stream<T, L::DropConsistency, B, O, R2> {
2505 if R::RETRIES_KIND == R2::RETRIES_KIND {
2506 Stream::new(
2507 self.location.drop_consistency(),
2508 self.ir_node.replace(HydroNode::Placeholder),
2509 )
2510 } else if R2::RETRIES_KIND == StreamRetry::AtLeastOnce {
2511 // We can always weaken the retries guarantee
2512 let target_location = self.location.drop_consistency();
2513 Stream::new(
2514 target_location.clone(),
2515 HydroNode::Cast {
2516 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2517 metadata: target_location
2518 .new_node_metadata(Stream::<T, L, B, O, R2>::collection_kind()),
2519 },
2520 )
2521 } else {
2522 let target_location = self.location.drop_consistency();
2523 let mut metadata =
2524 target_location.new_node_metadata(Stream::<T, L, B, O, R2>::collection_kind());
2525 metadata.op.sim_hook_id = nondet.take_hook().map(|hook| hook.id);
2526 Stream::new(
2527 target_location.clone(),
2528 HydroNode::ObserveNonDet {
2529 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2530 trusted: false,
2531 metadata,
2532 },
2533 )
2534 }
2535 }
2536
2537 // only for internal APIs that have been carefully vetted to ensure that the non-determinism
2538 // is not observable
2539 fn assume_retries_trusted<R2: Retries>(self, _nondet: NonDet) -> Stream<T, L, B, O, R2> {
2540 if R::RETRIES_KIND == R2::RETRIES_KIND {
2541 Stream::new(
2542 self.location.clone(),
2543 self.ir_node.replace(HydroNode::Placeholder),
2544 )
2545 } else if R2::RETRIES_KIND == StreamRetry::AtLeastOnce {
2546 // We can always weaken the retries guarantee
2547 Stream::new(
2548 self.location.clone(),
2549 HydroNode::Cast {
2550 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2551 metadata: self
2552 .location
2553 .new_node_metadata(Stream::<T, L, B, O, R2>::collection_kind()),
2554 },
2555 )
2556 } else {
2557 Stream::new(
2558 self.location.clone(),
2559 HydroNode::ObserveNonDet {
2560 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2561 trusted: true,
2562 metadata: self
2563 .location
2564 .new_node_metadata(Stream::<T, L, B, O, R2>::collection_kind()),
2565 },
2566 )
2567 }
2568 }
2569
2570 #[deprecated = "use `weaken_retries::<AtLeastOnce>()` instead"]
2571 /// Weakens the retries guarantee provided by the stream to [`AtLeastOnce`],
2572 /// which is always safe because that is the weakest possible guarantee.
2573 pub fn weakest_retries(self) -> Stream<T, L, B, O, AtLeastOnce> {
2574 self.weaken_retries::<AtLeastOnce>()
2575 }
2576
2577 /// Weakens the retries guarantee provided by the stream to `R2`, with the type-system
2578 /// enforcing that `R2` is weaker than the input retries guarantee.
2579 pub fn weaken_retries<R2: WeakerRetryThan<R>>(self) -> Stream<T, L, B, O, R2> {
2580 let nondet = nondet!(/** this is a weaker retry guarantee, so it is safe to assume */);
2581 self.assume_retries_trusted::<R2>(nondet)
2582 }
2583
2584 /// Strengthens the retry guarantee to `ExactlyOnce`, given that `R: IsExactlyOnce`, which
2585 /// implies that `R == ExactlyOnce`.
2586 pub fn make_exactly_once(self) -> Stream<T, L, B, O, ExactlyOnce>
2587 where
2588 R: IsExactlyOnce,
2589 {
2590 self.assume_retries_trusted(nondet!(/** no-op */))
2591 }
2592
2593 /// Strengthens the boundedness guarantee to `Bounded`, given that `B: IsBounded`, which
2594 /// implies that `B == Bounded`.
2595 pub fn make_bounded(self) -> Stream<T, L, Bounded, O, R>
2596 where
2597 B: IsBounded,
2598 {
2599 self.weaken_boundedness()
2600 }
2601
2602 /// Weakens the boundedness guarantee to an arbitrary boundedness `B2`, given that `B: IsBounded`,
2603 /// which implies that `B == Bounded`.
2604 pub fn weaken_boundedness<B2: Boundedness>(self) -> Stream<T, L, B2, O, R> {
2605 if B::BOUNDED == B2::BOUNDED {
2606 Stream::new(
2607 self.location.clone(),
2608 self.ir_node.replace(HydroNode::Placeholder),
2609 )
2610 } else {
2611 // We can always weaken the boundedness
2612 Stream::new(
2613 self.location.clone(),
2614 HydroNode::Cast {
2615 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2616 metadata: self
2617 .location
2618 .new_node_metadata(Stream::<T, L, B2, O, R>::collection_kind()),
2619 },
2620 )
2621 }
2622 }
2623}
2624
2625impl<'a, T, L, B: Boundedness, O: Ordering, R: Retries> Stream<&T, L, B, O, R>
2626where
2627 L: Location<'a>,
2628{
2629 /// Clone each element of the stream; akin to `map(q!(|d| d.clone()))`.
2630 ///
2631 /// # Example
2632 /// ```rust
2633 /// # #[cfg(feature = "deploy")] {
2634 /// # use hydro_lang::prelude::*;
2635 /// # use futures::StreamExt;
2636 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2637 /// process.source_iter(q!(&[1, 2, 3])).cloned()
2638 /// # }, |mut stream| async move {
2639 /// // 1, 2, 3
2640 /// # for w in vec![1, 2, 3] {
2641 /// # assert_eq!(stream.next().await.unwrap(), w);
2642 /// # }
2643 /// # }));
2644 /// # }
2645 /// ```
2646 pub fn cloned(self) -> Stream<T, L, B, O, R>
2647 where
2648 T: Clone,
2649 {
2650 self.map(q!(|d| d.clone()))
2651 }
2652}
2653
2654impl<'a, T, L, B: Boundedness, O: Ordering> Stream<T, L, B, O, ExactlyOnce>
2655where
2656 L: Location<'a>,
2657{
2658 /// Computes the number of elements in the stream as a [`Singleton`].
2659 ///
2660 /// # Example
2661 /// ```rust
2662 /// # #[cfg(feature = "deploy")] {
2663 /// # use hydro_lang::prelude::*;
2664 /// # use futures::StreamExt;
2665 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2666 /// let tick = process.tick();
2667 /// let numbers = process.source_iter(q!(vec![1, 2, 3, 4]));
2668 /// let batch = numbers.batch(&tick, nondet!(/** test */));
2669 /// batch.count().all_ticks()
2670 /// # }, |mut stream| async move {
2671 /// // 4
2672 /// # assert_eq!(stream.next().await.unwrap(), 4);
2673 /// # }));
2674 /// # }
2675 /// ```
2676 pub fn count(self) -> Singleton<usize, L, B::StreamToMonotone> {
2677 self.assume_ordering_trusted::<TotalOrder>(nondet!(
2678 /// Order does not affect eventual count, and also does not affect intermediate states.
2679 ))
2680 .fold(
2681 q!(|| 0usize),
2682 q!(
2683 |count, _| *count += 1,
2684 monotone = manual_proof!(/** += 1 is monotone */)
2685 ),
2686 )
2687 }
2688}
2689
2690impl<'a, T, L: Location<'a>, O: Ordering, R: Retries> Stream<T, L, Unbounded, O, R> {
2691 /// Produces a new stream that merges the elements of the two input streams.
2692 /// The result has [`NoOrder`] because the order of merging is not guaranteed.
2693 ///
2694 /// Currently, both input streams must be [`Unbounded`]. When the streams are
2695 /// [`Bounded`], you can use [`Stream::chain`] instead.
2696 ///
2697 /// # Example
2698 /// ```rust
2699 /// # #[cfg(feature = "deploy")] {
2700 /// # use hydro_lang::prelude::*;
2701 /// # use futures::StreamExt;
2702 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2703 /// let numbers: Stream<i32, _, Unbounded> = // 1, 2, 3, 4
2704 /// # process.source_iter(q!(vec![1, 2, 3, 4])).into();
2705 /// numbers.clone().map(q!(|x| x + 1)).merge_unordered(numbers)
2706 /// # }, |mut stream| async move {
2707 /// // 2, 3, 4, 5, and 1, 2, 3, 4 merged in unknown order
2708 /// # for w in vec![2, 3, 4, 5, 1, 2, 3, 4] {
2709 /// # assert_eq!(stream.next().await.unwrap(), w);
2710 /// # }
2711 /// # }));
2712 /// # }
2713 /// ```
2714 pub fn merge_unordered<O2: Ordering, R2: Retries>(
2715 self,
2716 other: Stream<T, L, Unbounded, O2, R2>,
2717 ) -> Stream<T, L, Unbounded, NoOrder, <R as MinRetries<R2>>::Min>
2718 where
2719 R: MinRetries<R2>,
2720 {
2721 Stream::new(
2722 self.location.clone(),
2723 HydroNode::Chain {
2724 first: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2725 second: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
2726 metadata: self.location.new_node_metadata(Stream::<
2727 T,
2728 L,
2729 Unbounded,
2730 NoOrder,
2731 <R as MinRetries<R2>>::Min,
2732 >::collection_kind()),
2733 },
2734 )
2735 }
2736
2737 /// Deprecated: use [`Stream::merge_unordered`] instead.
2738 #[deprecated(note = "use `merge_unordered` instead")]
2739 pub fn interleave<O2: Ordering, R2: Retries>(
2740 self,
2741 other: Stream<T, L, Unbounded, O2, R2>,
2742 ) -> Stream<T, L, Unbounded, NoOrder, <R as MinRetries<R2>>::Min>
2743 where
2744 R: MinRetries<R2>,
2745 {
2746 self.merge_unordered(other)
2747 }
2748}
2749
2750impl<'a, T, L: Location<'a>, B: Boundedness, R: Retries> Stream<T, L, B, TotalOrder, R> {
2751 /// Produces a new stream that combines the elements of the two input streams,
2752 /// preserving the relative order of elements within each input.
2753 ///
2754 /// # Non-Determinism
2755 /// The order in which elements *across* the two streams will be interleaved is
2756 /// non-deterministic, so the order of elements will vary across runs. If the output
2757 /// order is irrelevant, use [`Stream::merge_unordered`] instead, which is deterministic
2758 /// but emits an unordered stream. For deterministic first-then-second ordering on
2759 /// bounded streams, use [`Stream::chain`].
2760 ///
2761 /// In simulation tests, the interleaving decisions can be scripted by attaching a
2762 /// [`MergeOrderedHook`](crate::sim_hooks::MergeOrderedHook) to the guard via
2763 /// `nondet!(/** reason */ hook = my_hook)`.
2764 ///
2765 /// # Example
2766 /// ```rust
2767 /// # #[cfg(feature = "deploy")] {
2768 /// # use hydro_lang::prelude::*;
2769 /// # use futures::StreamExt;
2770 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2771 /// let numbers: Stream<i32, _, Unbounded> = // 1, 3
2772 /// # process.source_iter(q!(vec![1, 3])).into();
2773 /// numbers.clone().merge_ordered(numbers.map(q!(|x| x + 1)), nondet!(/** example */))
2774 /// # }, |mut stream| async move {
2775 /// // 1, 3 and 2, 4 in some order, preserving the original local order
2776 /// # for w in vec![1, 3, 2, 4] {
2777 /// # assert_eq!(stream.next().await.unwrap(), w);
2778 /// # }
2779 /// # }));
2780 /// # }
2781 /// ```
2782 pub fn merge_ordered<R2: Retries>(
2783 self,
2784 other: Stream<T, L, B, TotalOrder, R2>,
2785 mut nondet: NonDet<Option<crate::sim_hooks::MergeOrderedHook<T, B, L::SimHookScope>>>,
2786 ) -> Stream<T, L::DropConsistency, B, TotalOrder, <R as MinRetries<R2>>::Min>
2787 where
2788 R: MinRetries<R2>,
2789 {
2790 let target_location = self.location().drop_consistency();
2791 let mut metadata = target_location.new_node_metadata(Stream::<
2792 T,
2793 L::DropConsistency,
2794 B,
2795 TotalOrder,
2796 <R as MinRetries<R2>>::Min,
2797 >::collection_kind());
2798 metadata.op.sim_hook_id = nondet.take_hook().map(|hook| hook.id);
2799 Stream::new(
2800 target_location,
2801 HydroNode::MergeOrdered {
2802 first: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2803 second: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
2804 metadata,
2805 },
2806 )
2807 }
2808}
2809
2810impl<'a, T, L, B: Boundedness, O: Ordering, R: Retries> Stream<T, L, B, O, R>
2811where
2812 L: Location<'a>,
2813{
2814 /// Produces a new stream that emits the input elements in sorted order.
2815 ///
2816 /// The input stream can have any ordering guarantee, but the output stream
2817 /// will have a [`TotalOrder`] guarantee. This operator will block until all
2818 /// elements in the input stream are available, so it requires the input stream
2819 /// to be [`Bounded`].
2820 ///
2821 /// # Example
2822 /// ```rust
2823 /// # #[cfg(feature = "deploy")] {
2824 /// # use hydro_lang::prelude::*;
2825 /// # use futures::StreamExt;
2826 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2827 /// let tick = process.tick();
2828 /// let numbers = process.source_iter(q!(vec![4, 2, 3, 1]));
2829 /// let batch = numbers.batch(&tick, nondet!(/** test */));
2830 /// batch.sort().all_ticks()
2831 /// # }, |mut stream| async move {
2832 /// // 1, 2, 3, 4
2833 /// # for w in (1..5) {
2834 /// # assert_eq!(stream.next().await.unwrap(), w);
2835 /// # }
2836 /// # }));
2837 /// # }
2838 /// ```
2839 pub fn sort(self) -> Stream<T, L, Bounded, TotalOrder, R>
2840 where
2841 B: IsBounded,
2842 T: Ord,
2843 {
2844 let this = self.make_bounded();
2845 Stream::new(
2846 this.location.clone(),
2847 HydroNode::Sort {
2848 input: Box::new(this.ir_node.replace(HydroNode::Placeholder)),
2849 metadata: this
2850 .location
2851 .new_node_metadata(Stream::<T, L, Bounded, TotalOrder, R>::collection_kind()),
2852 },
2853 )
2854 }
2855
2856 /// Produces a new stream that first emits the elements of the `self` stream,
2857 /// and then emits the elements of the `other` stream. The output stream has
2858 /// a [`TotalOrder`] guarantee if and only if both input streams have a
2859 /// [`TotalOrder`] guarantee.
2860 ///
2861 /// Currently, both input streams must be [`Bounded`]. This operator will block
2862 /// on the first stream until all its elements are available. In a future version,
2863 /// we will relax the requirement on the `other` stream.
2864 ///
2865 /// # Example
2866 /// ```rust
2867 /// # #[cfg(feature = "deploy")] {
2868 /// # use hydro_lang::prelude::*;
2869 /// # use futures::StreamExt;
2870 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2871 /// let tick = process.tick();
2872 /// let numbers = process.source_iter(q!(vec![1, 2, 3, 4]));
2873 /// let batch = numbers.batch(&tick, nondet!(/** test */));
2874 /// batch.clone().map(q!(|x| x + 1)).chain(batch).all_ticks()
2875 /// # }, |mut stream| async move {
2876 /// // 2, 3, 4, 5, 1, 2, 3, 4
2877 /// # for w in vec![2, 3, 4, 5, 1, 2, 3, 4] {
2878 /// # assert_eq!(stream.next().await.unwrap(), w);
2879 /// # }
2880 /// # }));
2881 /// # }
2882 /// ```
2883 pub fn chain<O2: Ordering, R2: Retries, B2: Boundedness>(
2884 self,
2885 other: Stream<T, L, B2, O2, R2>,
2886 ) -> Stream<T, L, B2, <O as MinOrder<O2>>::Min, <R as MinRetries<R2>>::Min>
2887 where
2888 B: IsBounded,
2889 O: MinOrder<O2>,
2890 R: MinRetries<R2>,
2891 {
2892 check_matching_location(&self.location, &other.location);
2893
2894 Stream::new(
2895 self.location.clone(),
2896 HydroNode::Chain {
2897 first: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2898 second: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
2899 metadata: self.location.new_node_metadata(Stream::<
2900 T,
2901 L,
2902 B2,
2903 <O as MinOrder<O2>>::Min,
2904 <R as MinRetries<R2>>::Min,
2905 >::collection_kind()),
2906 },
2907 )
2908 }
2909
2910 /// Forms the cross-product (Cartesian product, cross-join) of the items in the 2 input streams.
2911 /// Unlike [`Stream::cross_product`], the output order is totally ordered when the inputs are
2912 /// because this is compiled into a nested loop.
2913 pub fn cross_product_nested_loop<T2, O2: Ordering + MinOrder<O>, R2: Retries>(
2914 self,
2915 other: Stream<T2, L, Bounded, O2, R2>,
2916 ) -> Stream<(T, T2), L, Bounded, <O2 as MinOrder<O>>::Min, <R as MinRetries<R2>>::Min>
2917 where
2918 B: IsBounded,
2919 T: Clone,
2920 T2: Clone,
2921 R: MinRetries<R2>,
2922 {
2923 let this = self.make_bounded();
2924 check_matching_location(&this.location, &other.location);
2925
2926 Stream::new(
2927 this.location.clone(),
2928 HydroNode::CrossProduct {
2929 left: Box::new(this.ir_node.replace(HydroNode::Placeholder)),
2930 right: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
2931 metadata: this.location.new_node_metadata(Stream::<
2932 (T, T2),
2933 L,
2934 Bounded,
2935 <O2 as MinOrder<O>>::Min,
2936 <R as MinRetries<R2>>::Min,
2937 >::collection_kind()),
2938 },
2939 )
2940 }
2941
2942 /// Creates a [`KeyedStream`] with the same set of keys as `keys`, but with the elements in
2943 /// `self` used as the values for *each* key.
2944 ///
2945 /// This is helpful when "broadcasting" a set of values so that all the keys have the same
2946 /// values. For example, it can be used to send the same set of elements to several cluster
2947 /// members, if the membership information is available as a [`KeyedSingleton`].
2948 ///
2949 /// # Example
2950 /// ```rust
2951 /// # #[cfg(feature = "deploy")] {
2952 /// # use hydro_lang::prelude::*;
2953 /// # use futures::StreamExt;
2954 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2955 /// # let tick = process.tick();
2956 /// let keyed_singleton = // { 1: (), 2: () }
2957 /// # process
2958 /// # .source_iter(q!(vec![(1, ()), (2, ())]))
2959 /// # .into_keyed()
2960 /// # .batch(&tick, nondet!(/** test */))
2961 /// # .first();
2962 /// let stream = // [ "a", "b" ]
2963 /// # process
2964 /// # .source_iter(q!(vec!["a".to_owned(), "b".to_owned()]))
2965 /// # .batch(&tick, nondet!(/** test */));
2966 /// stream.repeat_with_keys(keyed_singleton)
2967 /// # .entries().all_ticks()
2968 /// # }, |mut stream| async move {
2969 /// // { 1: ["a", "b" ], 2: ["a", "b"] }
2970 /// # let mut results = Vec::new();
2971 /// # for _ in 0..4 {
2972 /// # results.push(stream.next().await.unwrap());
2973 /// # }
2974 /// # results.sort();
2975 /// # assert_eq!(results, vec![(1, "a".to_owned()), (1, "b".to_owned()), (2, "a".to_owned()), (2, "b".to_owned())]);
2976 /// # }));
2977 /// # }
2978 /// ```
2979 pub fn repeat_with_keys<K, V2>(
2980 self,
2981 keys: KeyedSingleton<K, V2, L, Bounded>,
2982 ) -> KeyedStream<K, T, L, Bounded, O, R>
2983 where
2984 B: IsBounded,
2985 K: Clone,
2986 T: Clone,
2987 {
2988 keys.keys()
2989 .assume_ordering_trusted::<TotalOrder>(
2990 nondet!(/** keyed stream does not depend on ordering of keys */),
2991 )
2992 .cross_product_nested_loop(self.make_bounded())
2993 .into_keyed()
2994 }
2995
2996 /// Consumes a stream of `Future<T>`, resolving each future while blocking subgraph
2997 /// execution until all results are available. The output order is based on when futures
2998 /// complete, and may be different than the input order.
2999 ///
3000 /// Unlike [`Stream::resolve_futures`], which allows the subgraph to continue executing
3001 /// while futures are pending, this variant blocks until the futures resolve.
3002 ///
3003 /// # Example
3004 /// ```rust
3005 /// # #[cfg(feature = "deploy")] {
3006 /// # use std::collections::HashSet;
3007 /// # use futures::StreamExt;
3008 /// # use hydro_lang::prelude::*;
3009 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
3010 /// process
3011 /// .source_iter(q!([2, 3, 1, 9, 6, 5, 4, 7, 8]))
3012 /// .map(q!(|x| async move {
3013 /// tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
3014 /// x
3015 /// }))
3016 /// .resolve_futures_blocking()
3017 /// # },
3018 /// # |mut stream| async move {
3019 /// // 1, 2, 3, 4, 5, 6, 7, 8, 9 (in any order)
3020 /// # let mut output = HashSet::new();
3021 /// # for _ in 1..10 {
3022 /// # output.insert(stream.next().await.unwrap());
3023 /// # }
3024 /// # assert_eq!(
3025 /// # output,
3026 /// # HashSet::<i32>::from_iter(1..10)
3027 /// # );
3028 /// # },
3029 /// # ));
3030 /// # }
3031 /// ```
3032 pub fn resolve_futures_blocking(self) -> Stream<T::Output, L, B, NoOrder, R>
3033 where
3034 T: Future,
3035 {
3036 Stream::new(
3037 self.location.clone(),
3038 HydroNode::ResolveFuturesBlocking {
3039 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
3040 metadata: self
3041 .location
3042 .new_node_metadata(Stream::<T::Output, L, B, NoOrder, R>::collection_kind()),
3043 },
3044 )
3045 }
3046
3047 /// Returns a [`Singleton`] containing `true` if the stream has no elements, or `false` otherwise.
3048 ///
3049 /// # Example
3050 /// ```rust
3051 /// # #[cfg(feature = "deploy")] {
3052 /// # use hydro_lang::prelude::*;
3053 /// # use futures::StreamExt;
3054 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
3055 /// let tick = process.tick();
3056 /// let empty: Stream<i32, _, Bounded> = process
3057 /// .source_iter(q!(Vec::<i32>::new()))
3058 /// .batch(&tick, nondet!(/** test */));
3059 /// empty.is_empty().all_ticks()
3060 /// # }, |mut stream| async move {
3061 /// // true
3062 /// # assert_eq!(stream.next().await.unwrap(), true);
3063 /// # }));
3064 /// # }
3065 /// ```
3066 #[expect(clippy::wrong_self_convention, reason = "stream function naming")]
3067 pub fn is_empty(self) -> Singleton<bool, L, Bounded>
3068 where
3069 B: IsBounded,
3070 {
3071 self.make_bounded()
3072 .assume_ordering_trusted::<TotalOrder>(
3073 nondet!(/** is_empty intermediates unaffected by order */),
3074 )
3075 .first()
3076 .is_none()
3077 }
3078}
3079
3080impl<'a, K, V1, L, B: Boundedness, O: Ordering, R: Retries> Stream<(K, V1), L, B, O, R>
3081where
3082 L: Location<'a>,
3083{
3084 /// Given two streams of pairs `(K, V1)` and `(K, V2)`, produces a new stream of nested pairs `(K, (V1, V2))`
3085 /// by equi-joining the two streams on the key attribute `K`.
3086 ///
3087 /// When the right-hand side is [`Bounded`], the join accumulates the right side first
3088 /// and streams the left side through, preserving the left side's ordering. When both
3089 /// sides are [`Unbounded`], a symmetric hash join is used and ordering is [`NoOrder`].
3090 ///
3091 /// # Example
3092 /// ```rust
3093 /// # #[cfg(feature = "deploy")] {
3094 /// # use hydro_lang::prelude::*;
3095 /// # use std::collections::HashSet;
3096 /// # use futures::StreamExt;
3097 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
3098 /// let tick = process.tick();
3099 /// let stream1 = process.source_iter(q!(vec![(1, 'a'), (2, 'b')]));
3100 /// let stream2 = process.source_iter(q!(vec![(1, 'x'), (2, 'y')]));
3101 /// stream1.join(stream2)
3102 /// # }, |mut stream| async move {
3103 /// // (1, ('a', 'x')), (2, ('b', 'y'))
3104 /// # let expected = HashSet::from([(1, ('a', 'x')), (2, ('b', 'y'))]);
3105 /// # stream.map(|i| assert!(expected.contains(&i)));
3106 /// # }));
3107 /// # }
3108 pub fn join<V2, B2: Boundedness, O2: Ordering, R2: Retries>(
3109 self,
3110 n: Stream<(K, V2), L, B2, O2, R2>,
3111 ) -> Stream<(K, (V1, V2)), L, B, B2::PreserveOrderIfBounded<O>, <R as MinRetries<R2>>::Min>
3112 where
3113 K: Eq + Hash + Clone,
3114 R: MinRetries<R2>,
3115 V1: Clone,
3116 V2: Clone,
3117 {
3118 check_matching_location(&self.location, &n.location);
3119
3120 let ir_node = if B2::BOUNDED {
3121 HydroNode::JoinHalf {
3122 left: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
3123 right: Box::new(n.ir_node.replace(HydroNode::Placeholder)),
3124 metadata: self.location.new_node_metadata(Stream::<
3125 (K, (V1, V2)),
3126 L,
3127 B,
3128 B2::PreserveOrderIfBounded<O>,
3129 <R as MinRetries<R2>>::Min,
3130 >::collection_kind()),
3131 }
3132 } else {
3133 HydroNode::Join {
3134 left: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
3135 right: Box::new(n.ir_node.replace(HydroNode::Placeholder)),
3136 metadata: self.location.new_node_metadata(Stream::<
3137 (K, (V1, V2)),
3138 L,
3139 B,
3140 B2::PreserveOrderIfBounded<O>,
3141 <R as MinRetries<R2>>::Min,
3142 >::collection_kind()),
3143 }
3144 };
3145
3146 Stream::new(self.location.clone(), ir_node)
3147 }
3148
3149 /// Given a stream of pairs `(K, V1)` and a bounded stream of keys `K`,
3150 /// computes the anti-join of the items in the input -- i.e. returns
3151 /// unique items in the first input that do not have a matching key
3152 /// in the second input.
3153 ///
3154 /// # Example
3155 /// ```rust
3156 /// # #[cfg(feature = "deploy")] {
3157 /// # use hydro_lang::prelude::*;
3158 /// # use futures::StreamExt;
3159 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
3160 /// let tick = process.tick();
3161 /// let stream = process
3162 /// .source_iter(q!(vec![ (1, 'a'), (2, 'b'), (3, 'c'), (4, 'd') ]))
3163 /// .batch(&tick, nondet!(/** test */));
3164 /// let batch = process
3165 /// .source_iter(q!(vec![1, 2]))
3166 /// .batch(&tick, nondet!(/** test */));
3167 /// stream.anti_join(batch).all_ticks()
3168 /// # }, |mut stream| async move {
3169 /// # for w in vec![(3, 'c'), (4, 'd')] {
3170 /// # assert_eq!(stream.next().await.unwrap(), w);
3171 /// # }
3172 /// # }));
3173 /// # }
3174 pub fn anti_join<O2: Ordering, R2: Retries>(
3175 self,
3176 n: Stream<K, L, Bounded, O2, R2>,
3177 ) -> Stream<(K, V1), L, B, O, R>
3178 where
3179 K: Eq + Hash,
3180 {
3181 check_matching_location(&self.location, &n.location);
3182
3183 Stream::new(
3184 self.location.clone(),
3185 HydroNode::AntiJoin {
3186 pos: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
3187 neg: Box::new(n.ir_node.replace(HydroNode::Placeholder)),
3188 metadata: self
3189 .location
3190 .new_node_metadata(Stream::<(K, V1), L, B, O, R>::collection_kind()),
3191 },
3192 )
3193 }
3194}
3195
3196impl<'a, K, V, L: Location<'a>, B: Boundedness, O: Ordering, R: Retries>
3197 Stream<(K, V), L, B, O, R>
3198{
3199 /// Transforms this stream into a [`KeyedStream`], where the first element of each tuple
3200 /// is used as the key and the second element is added to the entries associated with that key.
3201 ///
3202 /// Because [`KeyedStream`] lazily groups values into buckets, this operator has zero computational
3203 /// cost and _does not_ require that the key type is hashable. Keyed streams are useful for
3204 /// performing grouped aggregations, but also for more precise ordering guarantees such as
3205 /// total ordering _within_ each group but no ordering _across_ groups.
3206 ///
3207 /// # Example
3208 /// ```rust
3209 /// # #[cfg(feature = "deploy")] {
3210 /// # use hydro_lang::prelude::*;
3211 /// # use futures::StreamExt;
3212 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
3213 /// process
3214 /// .source_iter(q!(vec![(1, 2), (1, 3), (2, 4)]))
3215 /// .into_keyed()
3216 /// # .entries()
3217 /// # }, |mut stream| async move {
3218 /// // { 1: [2, 3], 2: [4] }
3219 /// # for w in vec![(1, 2), (1, 3), (2, 4)] {
3220 /// # assert_eq!(stream.next().await.unwrap(), w);
3221 /// # }
3222 /// # }));
3223 /// # }
3224 /// ```
3225 pub fn into_keyed(self) -> KeyedStream<K, V, L, B, O, R> {
3226 KeyedStream::new(
3227 self.location.clone(),
3228 HydroNode::Cast {
3229 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
3230 metadata: self
3231 .location
3232 .new_node_metadata(KeyedStream::<K, V, L, B, O, R>::collection_kind()),
3233 },
3234 )
3235 }
3236}
3237
3238impl<'a, K, V, L, O: Ordering, R: Retries> Stream<(K, V), Tick<L>, Bounded, O, R>
3239where
3240 K: Eq + Hash,
3241 L: Location<'a>,
3242{
3243 /// Given a stream of pairs `(K, V)`, produces a new stream of unique keys `K`.
3244 /// # Example
3245 /// ```rust
3246 /// # #[cfg(feature = "deploy")] {
3247 /// # use hydro_lang::prelude::*;
3248 /// # use futures::StreamExt;
3249 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
3250 /// let tick = process.tick();
3251 /// let numbers = process.source_iter(q!(vec![(1, 2), (2, 3), (1, 3), (2, 4)]));
3252 /// let batch = numbers.batch(&tick, nondet!(/** test */));
3253 /// batch.keys().all_ticks()
3254 /// # }, |mut stream| async move {
3255 /// // 1, 2
3256 /// # assert_eq!(stream.next().await.unwrap(), 1);
3257 /// # assert_eq!(stream.next().await.unwrap(), 2);
3258 /// # }));
3259 /// # }
3260 /// ```
3261 pub fn keys(self) -> Stream<K, Tick<L>, Bounded, NoOrder, ExactlyOnce> {
3262 self.into_keyed()
3263 .fold(
3264 q!(|| ()),
3265 q!(
3266 |_, _| {},
3267 commutative = manual_proof!(/** values are ignored */),
3268 idempotent = manual_proof!(/** values are ignored */)
3269 ),
3270 )
3271 .keys()
3272 }
3273}
3274
3275impl<'a, T, L, B: Boundedness, O: Ordering, R: Retries> Stream<T, Atomic<L>, B, O, R>
3276where
3277 L: Location<'a>,
3278{
3279 /// Returns a stream corresponding to the latest batch of elements being atomically
3280 /// processed. These batches are guaranteed to be contiguous across ticks and preserve
3281 /// the order of the input.
3282 ///
3283 /// # Non-Determinism
3284 /// The batch boundaries are non-deterministic and may change across executions.
3285 pub fn batch_atomic<L2: Location<'a, DropConsistency = L::DropConsistency>>(
3286 self,
3287 tick: &Tick<L2>,
3288 mut nondet: NonDet<Option<crate::sim_hooks::BatchHook<T, O, R, L::SimHookScope>>>,
3289 ) -> Stream<T, Tick<L::DropConsistency>, Bounded, O, R> {
3290 assert_eq!(
3291 Location::id(tick.parent_location()),
3292 Location::id(self.location.tick.parent_location())
3293 );
3294
3295 let mut metadata =
3296 tick.new_node_metadata(Stream::<T, Tick<L>, Bounded, O, R>::collection_kind());
3297
3298 metadata.op.sim_hook_id = nondet.take_hook().map(|h| h.id);
3299 Stream::new(
3300 tick.drop_consistency(),
3301 HydroNode::Batch {
3302 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
3303 metadata,
3304 },
3305 )
3306 }
3307
3308 /// Yields the elements of this stream back into a top-level, asynchronous execution context.
3309 /// See [`Stream::atomic`] for more details.
3310 pub fn end_atomic(self) -> Stream<T, L, B, O, R> {
3311 Stream::new(
3312 self.location.tick.l.clone(),
3313 HydroNode::EndAtomic {
3314 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
3315 metadata: self
3316 .location
3317 .tick
3318 .l
3319 .new_node_metadata(Stream::<T, L, B, O, R>::collection_kind()),
3320 },
3321 )
3322 }
3323}
3324
3325impl<'a, F, T, L, B: Boundedness, O: Ordering, R: Retries> Stream<F, L, B, O, R>
3326where
3327 L: TopLevel<'a>,
3328 F: Future<Output = T>,
3329{
3330 /// Consumes a stream of `Future<T>`, produces a new stream of the resulting `T` outputs.
3331 /// Future outputs are produced as available, regardless of input arrival order.
3332 ///
3333 /// # Example
3334 /// ```rust
3335 /// # #[cfg(feature = "deploy")] {
3336 /// # use std::collections::HashSet;
3337 /// # use futures::StreamExt;
3338 /// # use hydro_lang::prelude::*;
3339 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
3340 /// process.source_iter(q!([2, 3, 1, 9, 6, 5, 4, 7, 8]))
3341 /// .map(q!(|x| async move {
3342 /// tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
3343 /// x
3344 /// }))
3345 /// .resolve_futures()
3346 /// # },
3347 /// # |mut stream| async move {
3348 /// // 1, 2, 3, 4, 5, 6, 7, 8, 9 (in any order)
3349 /// # let mut output = HashSet::new();
3350 /// # for _ in 1..10 {
3351 /// # output.insert(stream.next().await.unwrap());
3352 /// # }
3353 /// # assert_eq!(
3354 /// # output,
3355 /// # HashSet::<i32>::from_iter(1..10)
3356 /// # );
3357 /// # },
3358 /// # ));
3359 /// # }
3360 pub fn resolve_futures(self) -> Stream<T, L, Unbounded, NoOrder, R> {
3361 Stream::new(
3362 self.location.clone(),
3363 HydroNode::ResolveFutures {
3364 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
3365 metadata: self
3366 .location
3367 .new_node_metadata(Stream::<T, L, Unbounded, NoOrder, R>::collection_kind()),
3368 },
3369 )
3370 }
3371
3372 /// Consumes a stream of `Future<T>`, produces a new stream of the resulting `T` outputs.
3373 /// Future outputs are produced in the same order as the input stream.
3374 ///
3375 /// # Example
3376 /// ```rust
3377 /// # #[cfg(feature = "deploy")] {
3378 /// # use std::collections::HashSet;
3379 /// # use futures::StreamExt;
3380 /// # use hydro_lang::prelude::*;
3381 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
3382 /// process.source_iter(q!([2, 3, 1, 9, 6, 5, 4, 7, 8]))
3383 /// .map(q!(|x| async move {
3384 /// tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
3385 /// x
3386 /// }))
3387 /// .resolve_futures_ordered()
3388 /// # },
3389 /// # |mut stream| async move {
3390 /// // 2, 3, 1, 9, 6, 5, 4, 7, 8
3391 /// # let mut output = Vec::new();
3392 /// # for _ in 1..10 {
3393 /// # output.push(stream.next().await.unwrap());
3394 /// # }
3395 /// # assert_eq!(
3396 /// # output,
3397 /// # vec![2, 3, 1, 9, 6, 5, 4, 7, 8]
3398 /// # );
3399 /// # },
3400 /// # ));
3401 /// # }
3402 pub fn resolve_futures_ordered(self) -> Stream<T, L, Unbounded, O, R> {
3403 Stream::new(
3404 self.location.clone(),
3405 HydroNode::ResolveFuturesOrdered {
3406 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
3407 metadata: self
3408 .location
3409 .new_node_metadata(Stream::<T, L, Unbounded, O, R>::collection_kind()),
3410 },
3411 )
3412 }
3413}
3414
3415impl<'a, T, L, O: Ordering, R: Retries> Stream<T, Tick<L>, Bounded, O, R>
3416where
3417 L: Location<'a>,
3418{
3419 /// Asynchronously yields this batch of elements outside the tick as an unbounded stream,
3420 /// which will stream all the elements across _all_ tick iterations by concatenating the batches.
3421 pub fn all_ticks(self) -> Stream<T, L, Unbounded, O, R> {
3422 Stream::new(
3423 self.location.parent_location().clone(),
3424 HydroNode::YieldConcat {
3425 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
3426 metadata: self.location.parent_location().new_node_metadata(Stream::<
3427 T,
3428 L,
3429 Unbounded,
3430 O,
3431 R,
3432 >::collection_kind(
3433 )),
3434 },
3435 )
3436 }
3437
3438 /// Synchronously yields this batch of elements outside the tick as an unbounded stream,
3439 /// which will stream all the elements across _all_ tick iterations by concatenating the batches.
3440 ///
3441 /// Unlike [`Stream::all_ticks`], this preserves synchronous execution, as the output stream
3442 /// is emitted in an [`Atomic`] context that will process elements synchronously with the input
3443 /// stream's [`Tick`] context.
3444 pub fn all_ticks_atomic(self) -> Stream<T, Atomic<L>, Unbounded, O, R> {
3445 let out_location = Atomic {
3446 tick: self.location.clone(),
3447 };
3448
3449 Stream::new(
3450 out_location.clone(),
3451 HydroNode::YieldConcat {
3452 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
3453 metadata: out_location
3454 .new_node_metadata(Stream::<T, Atomic<L>, Unbounded, O, R>::collection_kind()),
3455 },
3456 )
3457 }
3458
3459 /// Transforms the stream using the given closure in "stateful" mode, where stateful operators
3460 /// such as `fold` retrain their memory across ticks rather than resetting across batches of
3461 /// input.
3462 ///
3463 /// This API is particularly useful for stateful computation on batches of data, such as
3464 /// maintaining an accumulated state that is up to date with the current batch.
3465 ///
3466 /// # Example
3467 /// ```rust
3468 /// # #[cfg(feature = "deploy")] {
3469 /// # use hydro_lang::prelude::*;
3470 /// # use futures::StreamExt;
3471 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
3472 /// let tick = process.tick();
3473 /// # // ticks are lazy by default, forces the second tick to run
3474 /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
3475 /// # let batch_first_tick = process
3476 /// # .source_iter(q!(vec![1, 2, 3, 4]))
3477 /// # .batch(&tick, nondet!(/** test */));
3478 /// # let batch_second_tick = process
3479 /// # .source_iter(q!(vec![5, 6, 7]))
3480 /// # .batch(&tick, nondet!(/** test */))
3481 /// # .defer_tick(); // appears on the second tick
3482 /// let input = // [1, 2, 3, 4 (first batch), 5, 6, 7 (second batch)]
3483 /// # batch_first_tick.chain(batch_second_tick);
3484 ///
3485 /// input.across_ticks(|s| s.count()).all_ticks()
3486 /// # }, |mut stream| async move {
3487 /// // [4, 7]
3488 /// assert_eq!(stream.next().await.unwrap(), 4);
3489 /// assert_eq!(stream.next().await.unwrap(), 7);
3490 /// # }));
3491 /// # }
3492 /// ```
3493 pub fn across_ticks<Out: BatchAtomic<'a>>(
3494 self,
3495 thunk: impl FnOnce(Stream<T, Atomic<L>, Unbounded, O, R>) -> Out,
3496 ) -> Out::Batched {
3497 thunk(self.all_ticks_atomic()).batched_atomic()
3498 }
3499
3500 /// Shifts the elements in `self` to the **next tick**, so that the returned stream at tick `T`
3501 /// always has the elements of `self` at tick `T - 1`.
3502 ///
3503 /// At tick `0`, the output stream is empty, since there is no previous tick.
3504 ///
3505 /// This operator enables stateful iterative processing with ticks, by sending data from one
3506 /// tick to the next. For example, you can use it to compare inputs across consecutive batches.
3507 ///
3508 /// # Example
3509 /// ```rust
3510 /// # #[cfg(feature = "deploy")] {
3511 /// # use hydro_lang::prelude::*;
3512 /// # use futures::StreamExt;
3513 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
3514 /// let tick = process.tick();
3515 /// // ticks are lazy by default, forces the second tick to run
3516 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
3517 ///
3518 /// let batch_first_tick = process
3519 /// .source_iter(q!(vec![1, 2, 3, 4]))
3520 /// .batch(&tick, nondet!(/** test */));
3521 /// let batch_second_tick = process
3522 /// .source_iter(q!(vec![0, 3, 4, 5, 6]))
3523 /// .batch(&tick, nondet!(/** test */))
3524 /// .defer_tick(); // appears on the second tick
3525 /// let changes_across_ticks = batch_first_tick.chain(batch_second_tick);
3526 ///
3527 /// changes_across_ticks.clone().filter_not_in(
3528 /// changes_across_ticks.defer_tick() // the elements from the previous tick
3529 /// ).all_ticks()
3530 /// # }, |mut stream| async move {
3531 /// // [1, 2, 3, 4 /* first tick */, 0, 5, 6 /* second tick */]
3532 /// # for w in vec![1, 2, 3, 4, 0, 5, 6] {
3533 /// # assert_eq!(stream.next().await.unwrap(), w);
3534 /// # }
3535 /// # }));
3536 /// # }
3537 /// ```
3538 pub fn defer_tick(self) -> Stream<T, Tick<L>, Bounded, O, R> {
3539 Stream::new(
3540 self.location.clone(),
3541 HydroNode::DeferTick {
3542 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
3543 metadata: self
3544 .location
3545 .new_node_metadata(Stream::<T, Tick<L>, Bounded, O, R>::collection_kind()),
3546 },
3547 )
3548 }
3549}
3550
3551#[cfg(test)]
3552mod tests {
3553 #[cfg(feature = "deploy")]
3554 use futures::{SinkExt, StreamExt};
3555 #[cfg(feature = "deploy")]
3556 use hydro_deploy::Deployment;
3557 #[cfg(feature = "deploy")]
3558 use serde::{Deserialize, Serialize};
3559 #[cfg(any(feature = "deploy", feature = "sim"))]
3560 use stageleft::q;
3561
3562 #[cfg(any(feature = "deploy", feature = "sim"))]
3563 use crate::compile::builder::FlowBuilder;
3564 #[cfg(feature = "deploy")]
3565 use crate::live_collections::sliced::sliced;
3566 #[cfg(feature = "deploy")]
3567 use crate::live_collections::stream::ExactlyOnce;
3568 #[cfg(feature = "sim")]
3569 use crate::live_collections::stream::NoOrder;
3570 #[cfg(any(feature = "deploy", feature = "sim"))]
3571 use crate::live_collections::stream::TotalOrder;
3572 #[cfg(any(feature = "deploy", feature = "sim"))]
3573 use crate::location::Location;
3574 #[cfg(feature = "sim")]
3575 use crate::networking::TCP;
3576 #[cfg(any(feature = "deploy", feature = "sim"))]
3577 use crate::nondet::nondet;
3578
3579 mod backtrace_chained_ops;
3580
3581 #[cfg(feature = "deploy")]
3582 struct P1 {}
3583 #[cfg(feature = "deploy")]
3584 struct P2 {}
3585
3586 #[cfg(feature = "deploy")]
3587 #[derive(Serialize, Deserialize, Debug)]
3588 struct SendOverNetwork {
3589 n: u32,
3590 }
3591
3592 #[cfg(feature = "deploy")]
3593 #[tokio::test]
3594 async fn first_ten_distributed() {
3595 use crate::networking::TCP;
3596
3597 let mut deployment = Deployment::new();
3598
3599 let mut flow = FlowBuilder::new();
3600 let first_node = flow.process::<P1>();
3601 let second_node = flow.process::<P2>();
3602 let external = flow.external::<P2>();
3603
3604 let numbers = first_node.source_iter(q!(0..10));
3605 let out_port = numbers
3606 .map(q!(|n| SendOverNetwork { n }))
3607 .send(&second_node, TCP.fail_stop().bincode())
3608 .send_bincode_external(&external);
3609
3610 let nodes = flow
3611 .with_process(&first_node, deployment.Localhost())
3612 .with_process(&second_node, deployment.Localhost())
3613 .with_external(&external, deployment.Localhost())
3614 .deploy(&mut deployment);
3615
3616 deployment.deploy().await.unwrap();
3617
3618 let mut external_out = nodes.connect(out_port).await;
3619
3620 deployment.start().await.unwrap();
3621
3622 for i in 0..10 {
3623 assert_eq!(external_out.next().await.unwrap().n, i);
3624 }
3625 }
3626
3627 #[cfg(feature = "deploy")]
3628 #[tokio::test]
3629 async fn first_cardinality() {
3630 let mut deployment = Deployment::new();
3631
3632 let mut flow = FlowBuilder::new();
3633 let node = flow.process::<()>();
3634 let external = flow.external::<()>();
3635
3636 let node_tick = node.tick();
3637 let count = node_tick
3638 .singleton(q!([1, 2, 3]))
3639 .into_stream()
3640 .flatten_ordered()
3641 .first()
3642 .into_stream()
3643 .count()
3644 .all_ticks()
3645 .send_bincode_external(&external);
3646
3647 let nodes = flow
3648 .with_process(&node, deployment.Localhost())
3649 .with_external(&external, deployment.Localhost())
3650 .deploy(&mut deployment);
3651
3652 deployment.deploy().await.unwrap();
3653
3654 let mut external_out = nodes.connect(count).await;
3655
3656 deployment.start().await.unwrap();
3657
3658 assert_eq!(external_out.next().await.unwrap(), 1);
3659 }
3660
3661 #[cfg(feature = "deploy")]
3662 #[tokio::test]
3663 async fn unbounded_reduce_remembers_state() {
3664 let mut deployment = Deployment::new();
3665
3666 let mut flow = FlowBuilder::new();
3667 let node = flow.process::<()>();
3668 let external = flow.external::<()>();
3669
3670 let (input_port, input) = node.source_external_bincode(&external);
3671 let out = input
3672 .reduce(q!(|acc, v| *acc += v))
3673 .sample_eager(nondet!(/** test */))
3674 .send_bincode_external(&external);
3675
3676 let nodes = flow
3677 .with_process(&node, deployment.Localhost())
3678 .with_external(&external, deployment.Localhost())
3679 .deploy(&mut deployment);
3680
3681 deployment.deploy().await.unwrap();
3682
3683 let mut external_in = nodes.connect(input_port).await;
3684 let mut external_out = nodes.connect(out).await;
3685
3686 deployment.start().await.unwrap();
3687
3688 external_in.send(1).await.unwrap();
3689 assert_eq!(external_out.next().await.unwrap(), 1);
3690
3691 external_in.send(2).await.unwrap();
3692 assert_eq!(external_out.next().await.unwrap(), 3);
3693 }
3694
3695 #[cfg(feature = "deploy")]
3696 #[tokio::test]
3697 async fn top_level_bounded_cross_singleton() {
3698 let mut deployment = Deployment::new();
3699
3700 let mut flow = FlowBuilder::new();
3701 let node = flow.process::<()>();
3702 let external = flow.external::<()>();
3703
3704 let (input_port, input) =
3705 node.source_external_bincode::<_, _, TotalOrder, ExactlyOnce>(&external);
3706
3707 let out = input
3708 .cross_singleton(
3709 node.source_iter(q!(vec![1, 2, 3]))
3710 .fold(q!(|| 0), q!(|acc, v| *acc += v)),
3711 )
3712 .send_bincode_external(&external);
3713
3714 let nodes = flow
3715 .with_process(&node, deployment.Localhost())
3716 .with_external(&external, deployment.Localhost())
3717 .deploy(&mut deployment);
3718
3719 deployment.deploy().await.unwrap();
3720
3721 let mut external_in = nodes.connect(input_port).await;
3722 let mut external_out = nodes.connect(out).await;
3723
3724 deployment.start().await.unwrap();
3725
3726 external_in.send(1).await.unwrap();
3727 assert_eq!(external_out.next().await.unwrap(), (1, 6));
3728
3729 external_in.send(2).await.unwrap();
3730 assert_eq!(external_out.next().await.unwrap(), (2, 6));
3731 }
3732
3733 #[cfg(feature = "deploy")]
3734 #[tokio::test]
3735 async fn top_level_bounded_reduce_cardinality() {
3736 let mut deployment = Deployment::new();
3737
3738 let mut flow = FlowBuilder::new();
3739 let node = flow.process::<()>();
3740 let external = flow.external::<()>();
3741
3742 let (input_port, input) =
3743 node.source_external_bincode::<_, _, TotalOrder, ExactlyOnce>(&external);
3744
3745 let out = sliced! {
3746 let input = use::batch(input, nondet!(/** test */));
3747 let v = use::snapshot(node.source_iter(q!(vec![1, 2, 3])).reduce(q!(|acc, v| *acc += v)), nondet!(/** test */));
3748 input.cross_singleton(v.into_stream().count())
3749 }
3750 .send_bincode_external(&external);
3751
3752 let nodes = flow
3753 .with_process(&node, deployment.Localhost())
3754 .with_external(&external, deployment.Localhost())
3755 .deploy(&mut deployment);
3756
3757 deployment.deploy().await.unwrap();
3758
3759 let mut external_in = nodes.connect(input_port).await;
3760 let mut external_out = nodes.connect(out).await;
3761
3762 deployment.start().await.unwrap();
3763
3764 external_in.send(1).await.unwrap();
3765 assert_eq!(external_out.next().await.unwrap(), (1, 1));
3766
3767 external_in.send(2).await.unwrap();
3768 assert_eq!(external_out.next().await.unwrap(), (2, 1));
3769 }
3770
3771 #[cfg(feature = "deploy")]
3772 #[tokio::test]
3773 async fn top_level_bounded_into_singleton_cardinality() {
3774 let mut deployment = Deployment::new();
3775
3776 let mut flow = FlowBuilder::new();
3777 let node = flow.process::<()>();
3778 let external = flow.external::<()>();
3779
3780 let (input_port, input) =
3781 node.source_external_bincode::<_, _, TotalOrder, ExactlyOnce>(&external);
3782
3783 let out = sliced! {
3784 let input = use::batch(input, nondet!(/** test */));
3785 let v = use::snapshot(node.source_iter(q!(vec![1, 2, 3])).reduce(q!(|acc, v| *acc += v)).into_singleton(), nondet!(/** test */));
3786 input.cross_singleton(v.into_stream().count())
3787 }
3788 .send_bincode_external(&external);
3789
3790 let nodes = flow
3791 .with_process(&node, deployment.Localhost())
3792 .with_external(&external, deployment.Localhost())
3793 .deploy(&mut deployment);
3794
3795 deployment.deploy().await.unwrap();
3796
3797 let mut external_in = nodes.connect(input_port).await;
3798 let mut external_out = nodes.connect(out).await;
3799
3800 deployment.start().await.unwrap();
3801
3802 external_in.send(1).await.unwrap();
3803 assert_eq!(external_out.next().await.unwrap(), (1, 1));
3804
3805 external_in.send(2).await.unwrap();
3806 assert_eq!(external_out.next().await.unwrap(), (2, 1));
3807 }
3808
3809 #[cfg(feature = "deploy")]
3810 #[tokio::test]
3811 async fn atomic_fold_replays_each_tick() {
3812 let mut deployment = Deployment::new();
3813
3814 let mut flow = FlowBuilder::new();
3815 let node = flow.process::<()>();
3816 let external = flow.external::<()>();
3817
3818 let (input_port, input) =
3819 node.source_external_bincode::<_, _, TotalOrder, ExactlyOnce>(&external);
3820 let tick = node.tick();
3821
3822 let out = input
3823 .batch(&tick, nondet!(/** test */))
3824 .cross_singleton(
3825 node.source_iter(q!(vec![1, 2, 3]))
3826 .atomic()
3827 .fold(q!(|| 0), q!(|acc, v| *acc += v))
3828 .snapshot_atomic(&tick, nondet!(/** test */)),
3829 )
3830 .all_ticks()
3831 .send_bincode_external(&external);
3832
3833 let nodes = flow
3834 .with_process(&node, deployment.Localhost())
3835 .with_external(&external, deployment.Localhost())
3836 .deploy(&mut deployment);
3837
3838 deployment.deploy().await.unwrap();
3839
3840 let mut external_in = nodes.connect(input_port).await;
3841 let mut external_out = nodes.connect(out).await;
3842
3843 deployment.start().await.unwrap();
3844
3845 external_in.send(1).await.unwrap();
3846 assert_eq!(external_out.next().await.unwrap(), (1, 6));
3847
3848 external_in.send(2).await.unwrap();
3849 assert_eq!(external_out.next().await.unwrap(), (2, 6));
3850 }
3851
3852 #[cfg(feature = "deploy")]
3853 #[tokio::test]
3854 async fn unbounded_scan_remembers_state() {
3855 let mut deployment = Deployment::new();
3856
3857 let mut flow = FlowBuilder::new();
3858 let node = flow.process::<()>();
3859 let external = flow.external::<()>();
3860
3861 let (input_port, input) = node.source_external_bincode(&external);
3862 let out = input
3863 .scan(
3864 q!(|| 0),
3865 q!(|acc, v| {
3866 *acc += v;
3867 Some(*acc)
3868 }),
3869 )
3870 .send_bincode_external(&external);
3871
3872 let nodes = flow
3873 .with_process(&node, deployment.Localhost())
3874 .with_external(&external, deployment.Localhost())
3875 .deploy(&mut deployment);
3876
3877 deployment.deploy().await.unwrap();
3878
3879 let mut external_in = nodes.connect(input_port).await;
3880 let mut external_out = nodes.connect(out).await;
3881
3882 deployment.start().await.unwrap();
3883
3884 external_in.send(1).await.unwrap();
3885 assert_eq!(external_out.next().await.unwrap(), 1);
3886
3887 external_in.send(2).await.unwrap();
3888 assert_eq!(external_out.next().await.unwrap(), 3);
3889 }
3890
3891 #[cfg(feature = "deploy")]
3892 #[tokio::test]
3893 async fn unbounded_enumerate_remembers_state() {
3894 let mut deployment = Deployment::new();
3895
3896 let mut flow = FlowBuilder::new();
3897 let node = flow.process::<()>();
3898 let external = flow.external::<()>();
3899
3900 let (input_port, input) = node.source_external_bincode(&external);
3901 let out = input.enumerate().send_bincode_external(&external);
3902
3903 let nodes = flow
3904 .with_process(&node, deployment.Localhost())
3905 .with_external(&external, deployment.Localhost())
3906 .deploy(&mut deployment);
3907
3908 deployment.deploy().await.unwrap();
3909
3910 let mut external_in = nodes.connect(input_port).await;
3911 let mut external_out = nodes.connect(out).await;
3912
3913 deployment.start().await.unwrap();
3914
3915 external_in.send(1).await.unwrap();
3916 assert_eq!(external_out.next().await.unwrap(), (0, 1));
3917
3918 external_in.send(2).await.unwrap();
3919 assert_eq!(external_out.next().await.unwrap(), (1, 2));
3920 }
3921
3922 #[cfg(feature = "deploy")]
3923 #[tokio::test]
3924 async fn unbounded_unique_remembers_state() {
3925 let mut deployment = Deployment::new();
3926
3927 let mut flow = FlowBuilder::new();
3928 let node = flow.process::<()>();
3929 let external = flow.external::<()>();
3930
3931 let (input_port, input) =
3932 node.source_external_bincode::<_, _, TotalOrder, ExactlyOnce>(&external);
3933 let out = input.unique().send_bincode_external(&external);
3934
3935 let nodes = flow
3936 .with_process(&node, deployment.Localhost())
3937 .with_external(&external, deployment.Localhost())
3938 .deploy(&mut deployment);
3939
3940 deployment.deploy().await.unwrap();
3941
3942 let mut external_in = nodes.connect(input_port).await;
3943 let mut external_out = nodes.connect(out).await;
3944
3945 deployment.start().await.unwrap();
3946
3947 external_in.send(1).await.unwrap();
3948 assert_eq!(external_out.next().await.unwrap(), 1);
3949
3950 external_in.send(2).await.unwrap();
3951 assert_eq!(external_out.next().await.unwrap(), 2);
3952
3953 external_in.send(1).await.unwrap();
3954 external_in.send(3).await.unwrap();
3955 assert_eq!(external_out.next().await.unwrap(), 3);
3956 }
3957
3958 #[cfg(feature = "sim")]
3959 #[test]
3960 #[should_panic]
3961 fn sim_batch_nondet_size() {
3962 let mut flow = FlowBuilder::new();
3963 let node = flow.process::<()>();
3964
3965 let (in_send, input) = node.sim_input::<_, TotalOrder, _>();
3966
3967 let tick = node.tick();
3968 let out_recv = input
3969 .batch(&tick, nondet!(/** test */))
3970 .count()
3971 .all_ticks()
3972 .sim_output();
3973
3974 flow.sim().exhaustive(async || {
3975 in_send.send(());
3976 in_send.send(());
3977 in_send.send(());
3978
3979 assert_eq!(out_recv.next().await, 3); // fails with nondet batching
3980 });
3981 }
3982
3983 #[cfg(feature = "sim")]
3984 #[test]
3985 fn sim_batch_preserves_order() {
3986 let mut flow = FlowBuilder::new();
3987 let node = flow.process::<()>();
3988
3989 let (in_send, input) = node.sim_input();
3990
3991 let tick = node.tick();
3992 let out_recv = input
3993 .batch(&tick, nondet!(/** test */))
3994 .all_ticks()
3995 .sim_output();
3996
3997 flow.sim().exhaustive(async || {
3998 in_send.send(1);
3999 in_send.send(2);
4000 in_send.send(3);
4001
4002 out_recv.assert_yields_only([1, 2, 3]).await;
4003 });
4004 }
4005
4006 #[cfg(feature = "sim")]
4007 #[test]
4008 #[should_panic]
4009 fn sim_batch_unordered_shuffles() {
4010 let mut flow = FlowBuilder::new();
4011 let node = flow.process::<()>();
4012
4013 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
4014
4015 let tick = node.tick();
4016 let batch = input.batch(&tick, nondet!(/** test */));
4017 let out_recv = batch
4018 .clone()
4019 .min()
4020 .zip(batch.max())
4021 .all_ticks()
4022 .sim_output();
4023
4024 flow.sim().exhaustive(async || {
4025 in_send.send_many_unordered([1, 2, 3]);
4026
4027 assert!(
4028 out_recv.collect::<Vec<_>>().await != vec![(1, 3), (2, 2)],
4029 "saw both (1, 3) and (2, 2), so batching must have shuffled the order"
4030 )
4031 });
4032 }
4033
4034 #[cfg(feature = "sim")]
4035 #[test]
4036 fn sim_batch_unordered_shuffles_count() {
4037 let mut flow = FlowBuilder::new();
4038 let node = flow.process::<()>();
4039
4040 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
4041
4042 let tick = node.tick();
4043 let batch = input.batch(&tick, nondet!(/** test */));
4044 let out_recv = batch.all_ticks().sim_output();
4045
4046 let instance_count = flow.sim().exhaustive(async || {
4047 in_send.send_many_unordered([1, 2, 3, 4]);
4048 out_recv.assert_yields_only_unordered([1, 2, 3, 4]).await;
4049 });
4050
4051 assert_eq!(
4052 instance_count,
4053 75 // ∑ (k=1 to 4) S(4,k) × k! = 75
4054 )
4055 }
4056
4057 #[cfg(feature = "sim")]
4058 #[test]
4059 #[should_panic]
4060 fn sim_observe_order_batched() {
4061 let mut flow = FlowBuilder::new();
4062 let node = flow.process::<()>();
4063
4064 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
4065
4066 let tick = node.tick();
4067 let batch = input.batch(&tick, nondet!(/** test */));
4068 let out_recv = batch
4069 .assume_ordering::<TotalOrder>(nondet!(/** test */))
4070 .all_ticks()
4071 .sim_output();
4072
4073 flow.sim().exhaustive(async || {
4074 in_send.send_many_unordered([1, 2, 3, 4]);
4075 out_recv.assert_yields_only([1, 2, 3, 4]).await; // fails with assume_ordering
4076 });
4077 }
4078
4079 #[cfg(feature = "sim")]
4080 #[test]
4081 fn sim_observe_order_batched_count() {
4082 let mut flow = FlowBuilder::new();
4083 let node = flow.process::<()>();
4084
4085 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
4086
4087 let tick = node.tick();
4088 let batch = input.batch(&tick, nondet!(/** test */));
4089 let out_recv = batch
4090 .assume_ordering::<TotalOrder>(nondet!(/** test */))
4091 .all_ticks()
4092 .sim_output();
4093
4094 let instance_count = flow.sim().exhaustive(async || {
4095 in_send.send_many_unordered([1, 2, 3, 4]);
4096 let _ = out_recv.collect::<Vec<_>>().await;
4097 });
4098
4099 assert_eq!(
4100 instance_count,
4101 192 // 4! * 2^{4 - 1}
4102 )
4103 }
4104
4105 #[cfg(feature = "sim")]
4106 #[test]
4107 fn sim_unordered_count_instance_count() {
4108 let mut flow = FlowBuilder::new();
4109 let node = flow.process::<()>();
4110
4111 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
4112
4113 let tick = node.tick();
4114 let out_recv = input
4115 .count()
4116 .snapshot(&tick, nondet!(/** test */))
4117 .all_ticks()
4118 .sim_output();
4119
4120 let instance_count = flow.sim().exhaustive(async || {
4121 in_send.send_many_unordered([1, 2, 3, 4]);
4122 assert_eq!(out_recv.collect::<Vec<_>>().await.last(), Some(&4));
4123 });
4124
4125 assert_eq!(
4126 instance_count,
4127 16 // 2^4, { 0, 1, 2, 3 } can be a snapshot and 4 is always included
4128 )
4129 }
4130
4131 #[cfg(feature = "sim")]
4132 #[test]
4133 fn sim_top_level_assume_ordering() {
4134 let mut flow = FlowBuilder::new();
4135 let node = flow.process::<()>();
4136
4137 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
4138
4139 let out_recv = input
4140 .assume_ordering::<TotalOrder>(nondet!(/** test */))
4141 .sim_output();
4142
4143 let instance_count = flow.sim().exhaustive(async || {
4144 in_send.send_many_unordered([1, 2, 3]);
4145 let mut out = out_recv.collect::<Vec<_>>().await;
4146 out.sort();
4147 assert_eq!(out, vec![1, 2, 3]);
4148 });
4149
4150 assert_eq!(instance_count, 6)
4151 }
4152
4153 #[cfg(feature = "sim")]
4154 #[test]
4155 fn sim_top_level_assume_ordering_cycle_back() {
4156 let mut flow = FlowBuilder::new();
4157 let node = flow.process::<()>();
4158 let node2 = flow.process::<()>();
4159
4160 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
4161
4162 let (complete_cycle_back, cycle_back) =
4163 node.forward_ref::<super::Stream<_, _, _, NoOrder>>();
4164 let ordered = input
4165 .merge_unordered(cycle_back)
4166 .assume_ordering::<TotalOrder>(nondet!(/** test */));
4167 complete_cycle_back.complete(
4168 ordered
4169 .clone()
4170 .map(q!(|v| v + 1))
4171 .filter(q!(|v| v % 2 == 1))
4172 .send(&node2, TCP.fail_stop().bincode())
4173 .send(&node, TCP.fail_stop().bincode()),
4174 );
4175
4176 let out_recv = ordered.sim_output();
4177
4178 let mut saw = false;
4179 let instance_count = flow.sim().exhaustive(async || {
4180 in_send.send_many_unordered([0, 2]);
4181 let out = out_recv.collect::<Vec<_>>().await;
4182
4183 if out.starts_with(&[0, 1, 2]) {
4184 saw = true;
4185 }
4186 });
4187
4188 assert!(saw, "did not see an instance with 0, 1, 2 in order");
4189 assert_eq!(instance_count, 6);
4190 }
4191
4192 #[cfg(feature = "sim")]
4193 #[test]
4194 fn sim_top_level_assume_ordering_cycle_back_tick() {
4195 let mut flow = FlowBuilder::new();
4196 let node = flow.process::<()>();
4197 let node2 = flow.process::<()>();
4198
4199 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
4200
4201 let (complete_cycle_back, cycle_back) =
4202 node.forward_ref::<super::Stream<_, _, _, NoOrder>>();
4203 let ordered = input
4204 .merge_unordered(cycle_back)
4205 .assume_ordering::<TotalOrder>(nondet!(/** test */));
4206 complete_cycle_back.complete(
4207 ordered
4208 .clone()
4209 .batch(&node.tick(), nondet!(/** test */))
4210 .all_ticks()
4211 .map(q!(|v| v + 1))
4212 .filter(q!(|v| v % 2 == 1))
4213 .send(&node2, TCP.fail_stop().bincode())
4214 .send(&node, TCP.fail_stop().bincode()),
4215 );
4216
4217 let out_recv = ordered.sim_output();
4218
4219 let mut saw = false;
4220 let instance_count = flow.sim().exhaustive(async || {
4221 in_send.send_many_unordered([0, 2]);
4222 let out = out_recv.collect::<Vec<_>>().await;
4223
4224 if out.starts_with(&[0, 1, 2]) {
4225 saw = true;
4226 }
4227 });
4228
4229 assert!(saw, "did not see an instance with 0, 1, 2 in order");
4230 assert_eq!(instance_count, 58);
4231 }
4232
4233 #[cfg(feature = "sim")]
4234 #[test]
4235 fn sim_top_level_assume_ordering_multiple() {
4236 let mut flow = FlowBuilder::new();
4237 let node = flow.process::<()>();
4238 let node2 = flow.process::<()>();
4239
4240 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
4241 let (_, input2) = node.sim_input::<_, NoOrder, _>();
4242
4243 let (complete_cycle_back, cycle_back) =
4244 node.forward_ref::<super::Stream<_, _, _, NoOrder>>();
4245 let input1_ordered = input
4246 .clone()
4247 .merge_unordered(cycle_back)
4248 .assume_ordering::<TotalOrder>(nondet!(/** test */));
4249 let foo = input1_ordered
4250 .clone()
4251 .map(q!(|v| v + 3))
4252 .weaken_ordering::<NoOrder>()
4253 .merge_unordered(input2)
4254 .assume_ordering::<TotalOrder>(nondet!(/** test */));
4255
4256 complete_cycle_back.complete(
4257 foo.filter(q!(|v| *v == 3))
4258 .send(&node2, TCP.fail_stop().bincode())
4259 .send(&node, TCP.fail_stop().bincode()),
4260 );
4261
4262 let out_recv = input1_ordered.sim_output();
4263
4264 let mut saw = false;
4265 let instance_count = flow.sim().exhaustive(async || {
4266 in_send.send_many_unordered([0, 1]);
4267 let out = out_recv.collect::<Vec<_>>().await;
4268
4269 if out.starts_with(&[0, 3, 1]) {
4270 saw = true;
4271 }
4272 });
4273
4274 assert!(saw, "did not see an instance with 0, 3, 1 in order");
4275 assert_eq!(instance_count, 15);
4276 }
4277
4278 #[cfg(feature = "sim")]
4279 #[test]
4280 fn sim_atomic_assume_ordering_cycle_back() {
4281 let mut flow = FlowBuilder::new();
4282 let node = flow.process::<()>();
4283 let node2 = flow.process::<()>();
4284
4285 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
4286
4287 let (complete_cycle_back, cycle_back) =
4288 node.forward_ref::<super::Stream<_, _, _, NoOrder>>();
4289 let ordered = input
4290 .merge_unordered(cycle_back)
4291 .atomic()
4292 .assume_ordering::<TotalOrder>(nondet!(/** test */))
4293 .end_atomic();
4294 complete_cycle_back.complete(
4295 ordered
4296 .clone()
4297 .map(q!(|v| v + 1))
4298 .filter(q!(|v| v % 2 == 1))
4299 .send(&node2, TCP.fail_stop().bincode())
4300 .send(&node, TCP.fail_stop().bincode()),
4301 );
4302
4303 let out_recv = ordered.sim_output();
4304
4305 let instance_count = flow.sim().exhaustive(async || {
4306 in_send.send_many_unordered([0, 2]);
4307 let out = out_recv.collect::<Vec<_>>().await;
4308 assert_eq!(out.len(), 4);
4309 });
4310 assert_eq!(instance_count, 22);
4311 }
4312
4313 #[cfg(feature = "deploy")]
4314 #[tokio::test]
4315 async fn partition_evens_odds() {
4316 let mut deployment = Deployment::new();
4317
4318 let mut flow = FlowBuilder::new();
4319 let node = flow.process::<()>();
4320 let external = flow.external::<()>();
4321
4322 let numbers = node.source_iter(q!(vec![1i32, 2, 3, 4, 5, 6]));
4323 let (evens, odds) = numbers.partition(q!(|x: &i32| x % 2 == 0));
4324 let evens_port = evens.send_bincode_external(&external);
4325 let odds_port = odds.send_bincode_external(&external);
4326
4327 let nodes = flow
4328 .with_process(&node, deployment.Localhost())
4329 .with_external(&external, deployment.Localhost())
4330 .deploy(&mut deployment);
4331
4332 deployment.deploy().await.unwrap();
4333
4334 let mut evens_out = nodes.connect(evens_port).await;
4335 let mut odds_out = nodes.connect(odds_port).await;
4336
4337 deployment.start().await.unwrap();
4338
4339 let mut even_results = Vec::new();
4340 for _ in 0..3 {
4341 even_results.push(evens_out.next().await.unwrap());
4342 }
4343 even_results.sort();
4344 assert_eq!(even_results, vec![2, 4, 6]);
4345
4346 let mut odd_results = Vec::new();
4347 for _ in 0..3 {
4348 odd_results.push(odds_out.next().await.unwrap());
4349 }
4350 odd_results.sort();
4351 assert_eq!(odd_results, vec![1, 3, 5]);
4352 }
4353
4354 #[cfg(feature = "deploy")]
4355 #[tokio::test]
4356 async fn unconsumed_inspect_still_runs() {
4357 use crate::deploy::DeployCrateWrapper;
4358
4359 let mut deployment = Deployment::new();
4360
4361 let mut flow = FlowBuilder::new();
4362 let node = flow.process::<()>();
4363
4364 // The return value of .inspect() is intentionally dropped.
4365 // Before the Null-root fix, this would silently do nothing.
4366 node.source_iter(q!(0..5))
4367 .inspect(q!(|x| println!("inspect: {}", x)));
4368
4369 let nodes = flow
4370 .with_process(&node, deployment.Localhost())
4371 .deploy(&mut deployment);
4372
4373 deployment.deploy().await.unwrap();
4374
4375 let mut stdout = nodes.get_process(&node).stdout();
4376
4377 deployment.start().await.unwrap();
4378
4379 let mut lines = Vec::new();
4380 for _ in 0..5 {
4381 lines.push(stdout.recv().await.unwrap());
4382 }
4383 lines.sort();
4384 assert_eq!(
4385 lines,
4386 vec![
4387 "inspect: 0",
4388 "inspect: 1",
4389 "inspect: 2",
4390 "inspect: 3",
4391 "inspect: 4",
4392 ]
4393 );
4394 }
4395
4396 #[cfg(feature = "deploy")]
4397 #[tokio::test]
4398 async fn unconsumed_inspect_alive_at_deploy_still_runs() {
4399 use crate::deploy::DeployCrateWrapper;
4400
4401 let mut deployment = Deployment::new();
4402
4403 let mut flow = FlowBuilder::new();
4404 let node = flow.process::<()>();
4405
4406 // The return value of .inspect() is bound to a variable that is still alive
4407 // when the flow is finalized by `deploy` below, so its `Drop` runs too late
4408 // to register a root the usual way. The FlowBuilder must yank the IR from
4409 // still-live collections when finalizing.
4410 let _inspected = node
4411 .source_iter(q!(0..5))
4412 .inspect(q!(|x| println!("inspect: {}", x)));
4413
4414 let nodes = flow
4415 .with_process(&node, deployment.Localhost())
4416 .deploy(&mut deployment);
4417
4418 deployment.deploy().await.unwrap();
4419
4420 let mut stdout = nodes.get_process(&node).stdout();
4421
4422 deployment.start().await.unwrap();
4423
4424 let mut lines = Vec::new();
4425 for _ in 0..5 {
4426 lines.push(stdout.recv().await.unwrap());
4427 }
4428 lines.sort();
4429 assert_eq!(
4430 lines,
4431 vec![
4432 "inspect: 0",
4433 "inspect: 1",
4434 "inspect: 2",
4435 "inspect: 3",
4436 "inspect: 4",
4437 ]
4438 );
4439 }
4440
4441 #[cfg(feature = "sim")]
4442 #[test]
4443 fn sim_limit() {
4444 let mut flow = FlowBuilder::new();
4445 let node = flow.process::<()>();
4446
4447 let (in_send, input) = node.sim_input();
4448
4449 let out_recv = input.limit(q!(3)).sim_output();
4450
4451 flow.sim().exhaustive(async || {
4452 in_send.send(1);
4453 in_send.send(2);
4454 in_send.send(3);
4455 in_send.send(4);
4456 in_send.send(5);
4457
4458 out_recv.assert_yields_only([1, 2, 3]).await;
4459 });
4460 }
4461
4462 #[cfg(feature = "sim")]
4463 #[test]
4464 fn sim_limit_zero() {
4465 let mut flow = FlowBuilder::new();
4466 let node = flow.process::<()>();
4467
4468 let (in_send, input) = node.sim_input();
4469
4470 let out_recv = input.limit(q!(0)).sim_output();
4471
4472 flow.sim().exhaustive(async || {
4473 in_send.send(1);
4474 in_send.send(2);
4475
4476 out_recv.assert_yields_only::<i32, _>([]).await;
4477 });
4478 }
4479
4480 #[cfg(feature = "sim")]
4481 #[test]
4482 fn sim_merge_ordered() {
4483 let mut flow = FlowBuilder::new();
4484 let node = flow.process::<()>();
4485
4486 let (in_send, input) = node.sim_input();
4487 let (in_send2, input2) = node.sim_input();
4488
4489 let out_recv = input
4490 .merge_ordered(input2, nondet!(/** test */))
4491 .sim_output();
4492
4493 let mut saw_out_of_order = false;
4494 let instances = flow.sim().exhaustive(async || {
4495 in_send.send(1);
4496 in_send.send(2);
4497 in_send2.send(3);
4498 in_send2.send(4);
4499
4500 let out = out_recv.collect::<Vec<_>>().await;
4501
4502 if out == [1, 3, 2, 4] {
4503 saw_out_of_order = true;
4504 }
4505
4506 // Assert ordering preservation: elements from each input must
4507 // appear in their original relative order.
4508 let mut first_elements = out.iter().filter(|v| **v <= 2).copied().collect::<Vec<_>>();
4509 let mut second_elements = out.iter().filter(|v| **v > 2).copied().collect::<Vec<_>>();
4510 assert_eq!(
4511 first_elements,
4512 vec![1, 2],
4513 "first input order violated: {:?}",
4514 out
4515 );
4516 assert_eq!(
4517 second_elements,
4518 vec![3, 4],
4519 "second input order violated: {:?}",
4520 out
4521 );
4522
4523 first_elements.append(&mut second_elements);
4524 first_elements.sort();
4525 assert_eq!(first_elements, vec![1, 2, 3, 4]);
4526 });
4527
4528 assert!(saw_out_of_order);
4529 assert_eq!(instances, 6);
4530 }
4531
4532 /// Tests that `merge_ordered` passes through elements when only one input
4533 /// has data.
4534 #[cfg(feature = "sim")]
4535 #[test]
4536 fn sim_merge_ordered_one_empty() {
4537 let mut flow = FlowBuilder::new();
4538 let node = flow.process::<()>();
4539
4540 let (in_send, input) = node.sim_input();
4541 let (_in_send2, input2) = node.sim_input();
4542
4543 let out_recv = input
4544 .merge_ordered(input2, nondet!(/** test */))
4545 .sim_output();
4546
4547 let instances = flow.sim().exhaustive(async || {
4548 in_send.send(1);
4549 in_send.send(2);
4550
4551 let out = out_recv.collect::<Vec<_>>().await;
4552 assert_eq!(out, vec![1, 2]);
4553 });
4554
4555 // Only one possible interleaving when one input is empty
4556 assert_eq!(instances, 1);
4557 }
4558
4559 /// Tests that `merge_ordered` correctly handles feedback cycles.
4560 /// An element output from `merge_ordered` is filtered and cycled back to
4561 /// one of its inputs. The one-at-a-time release must allow the cycled-back
4562 /// element to arrive and potentially be emitted before elements still
4563 /// waiting on the other input.
4564 #[cfg(feature = "sim")]
4565 #[test]
4566 fn sim_merge_ordered_cycle_back() {
4567 let mut flow = FlowBuilder::new();
4568 let node = flow.process::<()>();
4569
4570 let (in_send, input) = node.sim_input();
4571
4572 // Create a forward ref for the cycle back
4573 let (complete_cycle_back, cycle_back) =
4574 node.forward_ref::<super::Stream<_, _, _, TotalOrder>>();
4575
4576 // merge_ordered: input (external) with cycle_back
4577 let merged = input.merge_ordered(cycle_back, nondet!(/** test */));
4578
4579 // Cycle back: elements equal to 1 get mapped to 10 and fed back
4580 complete_cycle_back.complete(merged.clone().filter(q!(|v| *v == 1)).map(q!(|v| v * 10)));
4581
4582 let out_recv = merged.sim_output();
4583
4584 // Send 1 and 2. Element 1 should cycle back as 10.
4585 // Valid orderings must have 1 before 10 (since 10 depends on 1).
4586 let mut saw_cycle_before_second = false;
4587 flow.sim().exhaustive(async || {
4588 in_send.send(1);
4589 in_send.send(2);
4590
4591 let out = out_recv.collect::<Vec<_>>().await;
4592
4593 // 10 must always come after 1 (causal dependency)
4594 let pos_1 = out.iter().position(|v| *v == 1).unwrap();
4595 let pos_10 = out.iter().position(|v| *v == 10).unwrap();
4596 assert!(pos_1 < pos_10, "causal order violated: {:?}", out);
4597
4598 // Check if we see [1, 10, 2] — the cycled element beats the second input
4599 if out == [1, 10, 2] {
4600 saw_cycle_before_second = true;
4601 }
4602
4603 let mut sorted = out;
4604 sorted.sort();
4605 assert_eq!(sorted, vec![1, 2, 10]);
4606 });
4607
4608 assert!(
4609 saw_cycle_before_second,
4610 "never saw the cycled element arrive before the second input element"
4611 );
4612 }
4613
4614 /// Tests that `merge_ordered` correctly interleaves when one input has a
4615 /// delayed element. With a: [1, _delay_, 2] and b: [3, 4], the delayed
4616 /// element 2 should be able to appear after b's elements.
4617 #[cfg(feature = "sim")]
4618 #[test]
4619 fn sim_merge_ordered_delayed() {
4620 let mut flow = FlowBuilder::new();
4621 let node = flow.process::<()>();
4622
4623 let (in_send, input) = node.sim_input();
4624 let (in_send2, input2) = node.sim_input();
4625
4626 let out_recv = input
4627 .merge_ordered(input2, nondet!(/** test */))
4628 .sim_output();
4629
4630 let mut saw_delayed_interleaving = false;
4631 flow.sim().exhaustive(async || {
4632 // Send 1 from a, and 3, 4 from b
4633 in_send.send(1);
4634 in_send2.send(3);
4635 in_send2.send(4);
4636
4637 // Collect what's available so far
4638 let first_batch = out_recv.collect::<Vec<_>>().await;
4639
4640 // Now send the delayed element 2 from a
4641 in_send.send(2);
4642 let second_batch = out_recv.collect::<Vec<_>>().await;
4643
4644 let mut all: Vec<_> = first_batch
4645 .iter()
4646 .chain(second_batch.iter())
4647 .copied()
4648 .collect();
4649
4650 // Check if we saw [1, 3, 4, 2] — the delayed interleaving
4651 if all == [1, 3, 4, 2] {
4652 saw_delayed_interleaving = true;
4653 }
4654
4655 all.sort();
4656 assert_eq!(all, vec![1, 2, 3, 4]);
4657 });
4658
4659 assert!(saw_delayed_interleaving);
4660 }
4661
4662 /// Deploy test: `merge_ordered` with a delayed element on one input.
4663 /// Sends a=1, b=3, b=4, then after receiving those, sends a=2.
4664 /// Expects to see [1, 3, 4] first, then [2] — demonstrating that
4665 /// both inputs are pulled and the delayed element arrives later.
4666 #[cfg(feature = "deploy")]
4667 #[tokio::test]
4668 async fn deploy_merge_ordered_delayed() {
4669 let mut deployment = Deployment::new();
4670
4671 let mut flow = FlowBuilder::new();
4672 let node = flow.process::<()>();
4673 let external = flow.external::<()>();
4674
4675 let (input_a_port, input_a) = node.source_external_bincode(&external);
4676 let (input_b_port, input_b) = node.source_external_bincode(&external);
4677
4678 let out = input_a
4679 .assume_ordering(nondet!(/** test */))
4680 .merge_ordered(
4681 input_b.assume_ordering(nondet!(/** test */)),
4682 nondet!(/** test */),
4683 )
4684 .send_bincode_external(&external);
4685
4686 let nodes = flow
4687 .with_process(&node, deployment.Localhost())
4688 .with_external(&external, deployment.Localhost())
4689 .deploy(&mut deployment);
4690
4691 deployment.deploy().await.unwrap();
4692
4693 let mut ext_a = nodes.connect(input_a_port).await;
4694 let mut ext_b = nodes.connect(input_b_port).await;
4695 let mut ext_out = nodes.connect(out).await;
4696
4697 deployment.start().await.unwrap();
4698
4699 // Send a=1, b=3, b=4
4700 ext_a.send(1).await.unwrap();
4701 ext_b.send(3).await.unwrap();
4702 ext_b.send(4).await.unwrap();
4703
4704 // Collect the first 3 elements
4705 let mut received = Vec::new();
4706 for _ in 0..3 {
4707 received.push(ext_out.next().await.unwrap());
4708 }
4709
4710 // Now send the delayed a=2
4711 ext_a.send(2).await.unwrap();
4712 received.push(ext_out.next().await.unwrap());
4713
4714 // All elements should be present
4715 received.sort();
4716 assert_eq!(received, vec![1, 2, 3, 4]);
4717 }
4718
4719 #[cfg(feature = "deploy")]
4720 #[tokio::test]
4721 async fn monotone_fold_threshold() {
4722 use crate::properties::manual_proof;
4723
4724 let mut deployment = Deployment::new();
4725
4726 let mut flow = FlowBuilder::new();
4727 let node = flow.process::<()>();
4728 let external = flow.external::<()>();
4729
4730 let in_unbounded: super::Stream<_, _> =
4731 node.source_iter(q!(vec![1i32, 2, 3, 4, 5, 6])).into();
4732 let sum = in_unbounded.fold(
4733 q!(|| 0),
4734 q!(
4735 |sum, v| {
4736 *sum += v;
4737 },
4738 monotone = manual_proof!(/** test */)
4739 ),
4740 );
4741
4742 let threshold_out = sum
4743 .threshold_greater_or_equal(node.singleton(q!(7)))
4744 .send_bincode_external(&external);
4745
4746 let nodes = flow
4747 .with_process(&node, deployment.Localhost())
4748 .with_external(&external, deployment.Localhost())
4749 .deploy(&mut deployment);
4750
4751 deployment.deploy().await.unwrap();
4752
4753 let mut threshold_out = nodes.connect(threshold_out).await;
4754
4755 deployment.start().await.unwrap();
4756
4757 assert_eq!(threshold_out.next().await.unwrap(), 7);
4758 }
4759
4760 #[cfg(feature = "deploy")]
4761 #[tokio::test]
4762 async fn monotone_count_threshold() {
4763 let mut deployment = Deployment::new();
4764
4765 let mut flow = FlowBuilder::new();
4766 let node = flow.process::<()>();
4767 let external = flow.external::<()>();
4768
4769 let in_unbounded: super::Stream<_, _> =
4770 node.source_iter(q!(vec![1i32, 2, 3, 4, 5, 6])).into();
4771 let sum = in_unbounded.count();
4772
4773 let threshold_out = sum
4774 .threshold_greater_or_equal(node.singleton(q!(3)))
4775 .send_bincode_external(&external);
4776
4777 let nodes = flow
4778 .with_process(&node, deployment.Localhost())
4779 .with_external(&external, deployment.Localhost())
4780 .deploy(&mut deployment);
4781
4782 deployment.deploy().await.unwrap();
4783
4784 let mut threshold_out = nodes.connect(threshold_out).await;
4785
4786 deployment.start().await.unwrap();
4787
4788 assert_eq!(threshold_out.next().await.unwrap(), 3);
4789 }
4790
4791 #[cfg(feature = "deploy")]
4792 #[tokio::test]
4793 async fn monotone_map_order_preserving_threshold() {
4794 use crate::properties::manual_proof;
4795
4796 let mut deployment = Deployment::new();
4797
4798 let mut flow = FlowBuilder::new();
4799 let node = flow.process::<()>();
4800 let external = flow.external::<()>();
4801
4802 let in_unbounded: super::Stream<_, _> =
4803 node.source_iter(q!(vec![1i32, 2, 3, 4, 5, 6])).into();
4804 let sum = in_unbounded.fold(
4805 q!(|| 0),
4806 q!(
4807 |sum, v| {
4808 *sum += v;
4809 },
4810 monotone = manual_proof!(/** test */)
4811 ),
4812 );
4813
4814 // map with order_preserving should preserve monotonicity
4815 let doubled = sum.map(q!(
4816 |v| v * 2,
4817 order_preserving = manual_proof!(/** doubling preserves order */)
4818 ));
4819
4820 let threshold_out = doubled
4821 .threshold_greater_or_equal(node.singleton(q!(14)))
4822 .send_bincode_external(&external);
4823
4824 let nodes = flow
4825 .with_process(&node, deployment.Localhost())
4826 .with_external(&external, deployment.Localhost())
4827 .deploy(&mut deployment);
4828
4829 deployment.deploy().await.unwrap();
4830
4831 let mut threshold_out = nodes.connect(threshold_out).await;
4832
4833 deployment.start().await.unwrap();
4834
4835 assert_eq!(threshold_out.next().await.unwrap(), 14);
4836 }
4837
4838 // === Compile-time type tests for join/cross_product ordering ===
4839
4840 #[cfg(any(feature = "deploy", feature = "sim"))]
4841 mod join_ordering_type_tests {
4842 use crate::live_collections::boundedness::{Bounded, Unbounded};
4843 use crate::live_collections::stream::{ExactlyOnce, NoOrder, Stream, TotalOrder};
4844 use crate::location::{Location, Process};
4845
4846 #[expect(dead_code, reason = "compile-time type test")]
4847 fn join_unbounded_with_bounded_preserves_order<'a>(
4848 left: Stream<(i32, char), Process<'a>, Unbounded, TotalOrder, ExactlyOnce>,
4849 right: Stream<(i32, char), Process<'a>, Bounded, TotalOrder, ExactlyOnce>,
4850 ) -> Stream<(i32, (char, char)), Process<'a>, Unbounded, TotalOrder, ExactlyOnce> {
4851 left.join(right)
4852 }
4853
4854 #[expect(dead_code, reason = "compile-time type test")]
4855 fn join_unbounded_with_unbounded_is_no_order<'a>(
4856 left: Stream<(i32, char), Process<'a>, Unbounded, TotalOrder, ExactlyOnce>,
4857 right: Stream<(i32, char), Process<'a>, Unbounded, TotalOrder, ExactlyOnce>,
4858 ) -> Stream<(i32, (char, char)), Process<'a>, Unbounded, NoOrder, ExactlyOnce> {
4859 left.join(right)
4860 }
4861
4862 #[expect(dead_code, reason = "compile-time type test")]
4863 fn join_bounded_with_bounded_preserves_order<'a, L: Location<'a>>(
4864 left: Stream<(i32, char), L, Bounded, TotalOrder, ExactlyOnce>,
4865 right: Stream<(i32, char), L, Bounded, TotalOrder, ExactlyOnce>,
4866 ) -> Stream<(i32, (char, char)), L, Bounded, TotalOrder, ExactlyOnce> {
4867 left.join(right)
4868 }
4869
4870 #[expect(dead_code, reason = "compile-time type test")]
4871 fn join_unbounded_noorder_with_bounded<'a>(
4872 left: Stream<(i32, char), Process<'a>, Unbounded, NoOrder, ExactlyOnce>,
4873 right: Stream<(i32, char), Process<'a>, Bounded, NoOrder, ExactlyOnce>,
4874 ) -> Stream<(i32, (char, char)), Process<'a>, Unbounded, NoOrder, ExactlyOnce> {
4875 left.join(right)
4876 }
4877
4878 // === Compile-time type tests for cross_product ordering ===
4879
4880 #[expect(dead_code, reason = "compile-time type test")]
4881 fn cross_product_unbounded_with_bounded_preserves_order<'a>(
4882 left: Stream<i32, Process<'a>, Unbounded, TotalOrder, ExactlyOnce>,
4883 right: Stream<char, Process<'a>, Bounded, TotalOrder, ExactlyOnce>,
4884 ) -> Stream<(i32, char), Process<'a>, Unbounded, TotalOrder, ExactlyOnce> {
4885 left.cross_product(right)
4886 }
4887
4888 #[expect(dead_code, reason = "compile-time type test")]
4889 fn cross_product_bounded_with_bounded_preserves_order<'a>(
4890 left: Stream<i32, Process<'a>, Bounded, TotalOrder, ExactlyOnce>,
4891 right: Stream<char, Process<'a>, Bounded, TotalOrder, ExactlyOnce>,
4892 ) -> Stream<(i32, char), Process<'a>, Bounded, TotalOrder, ExactlyOnce> {
4893 left.cross_product(right)
4894 }
4895
4896 #[expect(dead_code, reason = "compile-time type test")]
4897 fn cross_product_unbounded_with_unbounded_is_no_order<'a>(
4898 left: Stream<i32, Process<'a>, Unbounded, TotalOrder, ExactlyOnce>,
4899 right: Stream<char, Process<'a>, Unbounded, TotalOrder, ExactlyOnce>,
4900 ) -> Stream<(i32, char), Process<'a>, Unbounded, NoOrder, ExactlyOnce> {
4901 left.cross_product(right)
4902 }
4903 } // mod join_ordering_type_tests
4904
4905 // === Runtime correctness tests for bounded join/cross_product ===
4906
4907 #[cfg(feature = "sim")]
4908 #[test]
4909 fn cross_product_mixed_boundedness_correctness() {
4910 use stageleft::q;
4911
4912 use crate::compile::builder::FlowBuilder;
4913 use crate::nondet::nondet;
4914
4915 let mut flow = FlowBuilder::new();
4916 let process = flow.process::<()>();
4917 let tick = process.tick();
4918
4919 let left = process.source_iter(q!(vec![1, 2]));
4920 let right = process
4921 .source_iter(q!(vec!['a', 'b']))
4922 .batch(&tick, nondet!(/** test */))
4923 .all_ticks();
4924
4925 let out = left.cross_product(right).sim_output();
4926
4927 flow.sim().exhaustive(async || {
4928 out.assert_yields_only_unordered(vec![(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')])
4929 .await;
4930 });
4931 }
4932
4933 #[cfg(feature = "sim")]
4934 #[test]
4935 fn join_mixed_boundedness_correctness() {
4936 use stageleft::q;
4937
4938 use crate::compile::builder::FlowBuilder;
4939 use crate::nondet::nondet;
4940
4941 let mut flow = FlowBuilder::new();
4942 let process = flow.process::<()>();
4943 let tick = process.tick();
4944
4945 let left = process.source_iter(q!(vec![(1, 'a'), (2, 'b')]));
4946 let right = process
4947 .source_iter(q!(vec![(1, 'x'), (2, 'y')]))
4948 .batch(&tick, nondet!(/** test */))
4949 .all_ticks();
4950
4951 let out = left.join(right).sim_output();
4952
4953 flow.sim().exhaustive(async || {
4954 out.assert_yields_only_unordered(vec![(1, ('a', 'x')), (2, ('b', 'y'))])
4955 .await;
4956 });
4957 }
4958
4959 #[cfg(feature = "sim")]
4960 #[test]
4961 fn sim_merge_unordered_independent_atomics() {
4962 let mut flow = FlowBuilder::new();
4963 let node = flow.process::<()>();
4964
4965 let (in1_send, input1) = node.sim_input::<_, TotalOrder, _>();
4966 let (in2_send, input2) = node.sim_input::<_, TotalOrder, _>();
4967
4968 let out = input1
4969 .atomic()
4970 .merge_unordered(input2.atomic())
4971 .end_atomic()
4972 .sim_output();
4973
4974 flow.sim().exhaustive(async || {
4975 in1_send.send(1);
4976 in2_send.send(2);
4977
4978 out.assert_yields_only_unordered(vec![1, 2]).await;
4979 });
4980 }
4981
4982 #[cfg(feature = "deploy")]
4983 #[tokio::test]
4984 async fn test_stream_ref() {
4985 let mut deployment = Deployment::new();
4986
4987 let mut flow = FlowBuilder::new();
4988 let external = flow.external::<()>();
4989 let p1 = flow.process::<()>();
4990
4991 // Create a bounded stream (source_iter is bounded within a tick)
4992 let my_stream = p1.source_iter(q!(1..=5i32));
4993
4994 let stream_ref = my_stream.by_ref();
4995
4996 // Use the stream ref to get the vec's length
4997 let out_port = p1
4998 .source_iter(q!([()]))
4999 .map(q!(|_| stream_ref.len() as i32))
5000 .send_bincode_external(&external);
5001
5002 // Also consume the stream via pipe
5003 my_stream.for_each(q!(|_| {}));
5004
5005 let nodes = flow
5006 .with_default_optimize()
5007 .with_process(&p1, deployment.Localhost())
5008 .with_external(&external, deployment.Localhost())
5009 .deploy(&mut deployment);
5010
5011 deployment.deploy().await.unwrap();
5012
5013 let mut out_recv = nodes.connect(out_port).await;
5014
5015 deployment.start().await.unwrap();
5016
5017 let result = out_recv.next().await.unwrap();
5018 // stream has 5 elements
5019 assert_eq!(result, 5);
5020 }
5021
5022 #[cfg(feature = "deploy")]
5023 #[tokio::test]
5024 async fn test_stream_ref_contents() {
5025 let mut deployment = Deployment::new();
5026
5027 let mut flow = FlowBuilder::new();
5028 let external = flow.external::<()>();
5029 let p1 = flow.process::<()>();
5030
5031 // Create a bounded stream
5032 let my_stream = p1.source_iter(q!(1..=3i32));
5033
5034 let stream_ref = my_stream.by_ref();
5035
5036 // Sum the referenced vec's contents
5037 let out_port = p1
5038 .source_iter(q!([()]))
5039 .map(q!(|_| stream_ref.iter().sum::<i32>()))
5040 .send_bincode_external(&external);
5041
5042 my_stream.for_each(q!(|_| {}));
5043
5044 let nodes = flow
5045 .with_default_optimize()
5046 .with_process(&p1, deployment.Localhost())
5047 .with_external(&external, deployment.Localhost())
5048 .deploy(&mut deployment);
5049
5050 deployment.deploy().await.unwrap();
5051
5052 let mut out_recv = nodes.connect(out_port).await;
5053
5054 deployment.start().await.unwrap();
5055
5056 let result = out_recv.next().await.unwrap();
5057 // sum of 1+2+3 = 6
5058 assert_eq!(result, 6);
5059 }
5060
5061 #[cfg(feature = "deploy")]
5062 #[tokio::test]
5063 async fn test_stream_ref_no_consumer() {
5064 let mut deployment = Deployment::new();
5065
5066 let mut flow = FlowBuilder::new();
5067 let external = flow.external::<()>();
5068 let p1 = flow.process::<()>();
5069
5070 // Create a bounded stream — no pipe consumer, only ref
5071 let my_stream = p1.source_iter(q!(1..=4i32));
5072
5073 let stream_ref = my_stream.by_ref();
5074
5075 let out_port = p1
5076 .source_iter(q!([()]))
5077 .map(q!(|_| stream_ref.len() as i32))
5078 .send_bincode_external(&external);
5079
5080 let nodes = flow
5081 .with_default_optimize()
5082 .with_process(&p1, deployment.Localhost())
5083 .with_external(&external, deployment.Localhost())
5084 .deploy(&mut deployment);
5085
5086 deployment.deploy().await.unwrap();
5087
5088 let mut out_recv = nodes.connect(out_port).await;
5089
5090 deployment.start().await.unwrap();
5091
5092 let result = out_recv.next().await.unwrap();
5093 assert_eq!(result, 4);
5094 }
5095
5096 #[cfg(feature = "deploy")]
5097 #[tokio::test]
5098 async fn test_stream_mut() {
5099 let mut deployment = Deployment::new();
5100
5101 let mut flow = FlowBuilder::new();
5102 let external = flow.external::<()>();
5103 let p1 = flow.process::<()>();
5104
5105 // Create a bounded stream
5106 let my_stream = p1.source_iter(q!(1..=5i32));
5107
5108 let stream_mut = my_stream.by_mut();
5109
5110 // Mutably reference the buffer to retain only items > 3
5111 let out_port = p1
5112 .source_iter(q!([()]))
5113 .map(q!(|_| {
5114 stream_mut.retain(|x| *x > 3);
5115 stream_mut.len() as i32
5116 }))
5117 .send_bincode_external(&external);
5118
5119 my_stream.for_each(q!(|_| {}));
5120
5121 let nodes = flow
5122 .with_default_optimize()
5123 .with_process(&p1, deployment.Localhost())
5124 .with_external(&external, deployment.Localhost())
5125 .deploy(&mut deployment);
5126
5127 deployment.deploy().await.unwrap();
5128
5129 let mut out_recv = nodes.connect(out_port).await;
5130
5131 deployment.start().await.unwrap();
5132
5133 let result = out_recv.next().await.unwrap();
5134 // After retain(> 3): [4, 5] => len = 2
5135 assert_eq!(result, 2);
5136 }
5137
5138 /// A map with a mut singleton ref on an unordered input should produce > 1
5139 /// simulation instance because the ordering of elements through the mut closure
5140 /// is non-deterministic.
5141 #[cfg(feature = "sim")]
5142 #[test]
5143 fn sim_map_with_mut_on_unordered_explores_multiple_states() {
5144 use crate::live_collections::sliced::sliced;
5145 use crate::live_collections::stream::ExactlyOnce;
5146 use crate::properties::manual_proof;
5147
5148 let mut flow = FlowBuilder::new();
5149 let node = flow.process::<()>();
5150
5151 let (trigger_send, trigger) = node.sim_input::<i32, TotalOrder, ExactlyOnce>();
5152
5153 let out_recv = sliced! {
5154 let batch = use::batch(trigger, nondet!(/** test */));
5155 let counter = batch.location().source_iter(q!(vec![0i32]))
5156 .fold(q!(|| 0i32), q!(|acc, v| *acc += v));
5157 let counter_mut = counter.by_mut();
5158 let items = batch.location().source_iter(q!(vec![1i32, 2])).weaken_ordering::<NoOrder>();
5159 items.map(q!(
5160 |x| {
5161 *counter_mut += x;
5162 *counter_mut
5163 },
5164 commutative = manual_proof!(/** test */)
5165 ))
5166 }
5167 .sim_output();
5168
5169 let count = flow.sim().exhaustive(async || {
5170 trigger_send.send(1);
5171 let _all: Vec<i32> = out_recv.collect_sorted().await;
5172 });
5173
5174 assert_eq!(
5175 count, 2,
5176 "Expected 2 simulation instances due to mut on unordered input, got {}",
5177 count
5178 );
5179 }
5180
5181 /// A `scan` closure that captures a bounded singleton by reference should compile,
5182 /// run correctly, and (because the input is totally ordered) explore a single
5183 /// simulation instance.
5184 #[cfg(feature = "sim")]
5185 #[test]
5186 fn sim_scan_with_ref_capture() {
5187 use crate::live_collections::sliced::sliced;
5188 use crate::live_collections::stream::ExactlyOnce;
5189
5190 let mut flow = FlowBuilder::new();
5191 let node = flow.process::<()>();
5192
5193 let (trigger_send, trigger) = node.sim_input::<i32, TotalOrder, ExactlyOnce>();
5194
5195 let out_recv = sliced! {
5196 let batch = use::batch(trigger, nondet!(/** test */));
5197 let offset = batch
5198 .location()
5199 .source_iter(q!(vec![10i32]))
5200 .fold(q!(|| 0i32), q!(|acc, v| *acc += v));
5201 let offset_ref = offset.by_ref();
5202 batch
5203 .location()
5204 .source_iter(q!(vec![1i32, 2, 3]))
5205 .scan(
5206 q!(|| 0i32),
5207 q!(move |acc: &mut i32, x| {
5208 *acc += x + *offset_ref;
5209 Some(*acc)
5210 }),
5211 )
5212 }
5213 .sim_output();
5214
5215 let count = flow.sim().exhaustive(async || {
5216 trigger_send.send(1);
5217 let all: Vec<i32> = out_recv.collect().await;
5218 // offset = 10, running accumulator starts at 0:
5219 // x=1: acc += 1 + 10 = 11 -> 11
5220 // x=2: acc += 2 + 10 = 12 -> 23
5221 // x=3: acc += 3 + 10 = 13 -> 36
5222 assert_eq!(all, vec![11, 23, 36]);
5223 });
5224
5225 assert_eq!(
5226 count, 1,
5227 "Expected a single simulation instance for a totally-ordered scan, got {}",
5228 count
5229 );
5230 }
5231
5232 /// A map with a mut singleton ref on a top-level unordered input should produce > 1
5233 /// simulation instance. Currently panics because `observe_nondet` doesn't support
5234 /// top-level bounded inputs yet.
5235 #[cfg(feature = "sim")]
5236 #[test]
5237 #[ignore = "observe_nondet not yet supported for top-level bounded inputs (https://github.com/hydro-project/hydro/issues/2950)"]
5238 fn sim_map_with_mut_on_unordered_top_level() {
5239 use crate::properties::manual_proof;
5240
5241 let mut flow = FlowBuilder::new();
5242 let node = flow.process::<()>();
5243
5244 let counter = node
5245 .source_iter(q!(vec![0i32]))
5246 .fold(q!(|| 0i32), q!(|acc, v| *acc += v));
5247 let counter_mut = counter.by_mut();
5248
5249 let out_recv = node
5250 .source_iter(q!(vec![1i32, 2]))
5251 .weaken_ordering::<NoOrder>()
5252 .map(q!(
5253 |x| {
5254 *counter_mut += x;
5255 *counter_mut
5256 },
5257 commutative = manual_proof!(/** test */)
5258 ))
5259 .assume_ordering::<TotalOrder>(nondet!(/** test */))
5260 .sim_output();
5261
5262 counter.into_stream().for_each(q!(|_| {}));
5263
5264 let count = flow.sim().exhaustive(async || {
5265 let _all: Vec<i32> = out_recv.collect().await;
5266 });
5267
5268 assert_eq!(
5269 count, 2,
5270 "Expected 2 simulation instances due to mut on unordered input, got {}",
5271 count
5272 );
5273 }
5274}