hydro_lang/live_collections/keyed_stream/mod.rs
1//! Definitions for the [`KeyedStream`] live collection.
2
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::hash::Hash;
6use std::marker::PhantomData;
7use std::ops::Deref;
8use std::rc::Rc;
9
10use stageleft::{IntoQuotedMut, QuotedWithContext, QuotedWithContextWithProps, q};
11
12use super::OperatorContext;
13use super::boundedness::{Bounded, Boundedness, IsBounded, Unbounded};
14use super::keyed_singleton::KeyedSingleton;
15use super::optional::Optional;
16use super::stream::{
17 ExactlyOnce, IsExactlyOnce, IsOrdered, MinOrder, MinRetries, NoOrder, Stream, TotalOrder,
18};
19use crate::compile::builder::{CycleId, FlowState};
20use crate::compile::ir::{
21 CollectionKind, HydroIrOpMetadata, HydroNode, HydroRoot, SharedNode, StreamOrder, StreamRetry,
22};
23#[cfg(stageleft_runtime)]
24use crate::forward_handle::{CycleCollection, ReceiverComplete};
25use crate::forward_handle::{ForwardRef, TickCycle};
26use crate::live_collections::batch_atomic::BatchAtomic;
27use crate::live_collections::keyed_singleton::KeyedSingletonBound;
28use crate::live_collections::stream::{
29 AtLeastOnce, Ordering, Retries, WeakerOrderingThan, WeakerRetryThan,
30};
31#[cfg(stageleft_runtime)]
32use crate::location::dynamic::{DynLocation, LocationId};
33use crate::location::tick::DeferTick;
34use crate::location::{Atomic, Location, Tick, check_matching_location};
35use crate::manual_expr::ManualExpr;
36use crate::nondet::{NonDet, nondet};
37use crate::properties::{
38 AggFuncAlgebra, ApplyMonotoneKeyedStream, ValidCommutativityFor, ValidIdempotenceFor,
39 manual_proof,
40};
41
42pub mod networking;
43
44/// Streaming elements of type `V` grouped by a key of type `K`.
45///
46/// Keyed Streams capture streaming elements of type `V` grouped by a key of type `K`, where the
47/// order of keys is non-deterministic but the order *within* each group may be deterministic.
48///
49/// Although keyed streams are conceptually grouped by keys, values are not immediately grouped
50/// into buckets when constructing a keyed stream. Instead, keyed streams defer grouping until an
51/// operator such as [`KeyedStream::fold`] is called, which requires `K: Hash + Eq`.
52///
53/// Type Parameters:
54/// - `K`: the type of the key for each group
55/// - `V`: the type of the elements inside each group
56/// - `Loc`: the [`Location`] where the keyed stream is materialized
57/// - `Bound`: tracks whether the entries are [`Bounded`] (local and finite) or [`Unbounded`] (asynchronous and possibly infinite)
58/// - `Order`: tracks whether the elements within each group have deterministic order
59/// ([`TotalOrder`]) or not ([`NoOrder`])
60/// - `Retries`: tracks whether the elements within each group have deterministic cardinality
61/// ([`ExactlyOnce`]) or may have non-deterministic retries ([`crate::live_collections::stream::AtLeastOnce`])
62pub struct KeyedStream<
63 K,
64 V,
65 Loc,
66 Bound: Boundedness = Unbounded,
67 Order: Ordering = TotalOrder,
68 Retry: Retries = ExactlyOnce,
69> {
70 pub(crate) location: Loc,
71 pub(crate) ir_node: Rc<RefCell<HydroNode>>,
72 pub(crate) flow_state: FlowState,
73
74 _phantom: PhantomData<(K, V, Loc, Bound, Order, Retry)>,
75}
76
77impl<K, V, L, B: Boundedness, O: Ordering, R: Retries> Drop for KeyedStream<K, V, L, B, O, R> {
78 fn drop(&mut self) {
79 let ir_node = self.ir_node.replace(HydroNode::Placeholder);
80 if !matches!(ir_node, HydroNode::Placeholder) && !ir_node.is_shared_with_others() {
81 self.flow_state.borrow_mut().try_push_root(HydroRoot::Null {
82 input: Box::new(ir_node),
83 op_metadata: HydroIrOpMetadata::new(),
84 });
85 }
86 }
87}
88
89impl<'a, K, V, L, O: Ordering, R: Retries> From<KeyedStream<K, V, L, Bounded, O, R>>
90 for KeyedStream<K, V, L, Unbounded, O, R>
91where
92 L: Location<'a>,
93{
94 fn from(stream: KeyedStream<K, V, L, Bounded, O, R>) -> KeyedStream<K, V, L, Unbounded, O, R> {
95 let new_meta = stream
96 .location
97 .new_node_metadata(KeyedStream::<K, V, L, Unbounded, O, R>::collection_kind());
98
99 let flow_state = stream.flow_state.clone();
100 KeyedStream {
101 location: stream.location.clone(),
102 ir_node: crate::live_collections::tracked_ir_node(
103 &flow_state,
104 HydroNode::Cast {
105 inner: Box::new(stream.ir_node.replace(HydroNode::Placeholder)),
106 metadata: new_meta,
107 },
108 ),
109 flow_state,
110 _phantom: PhantomData,
111 }
112 }
113}
114
115impl<'a, K, V, L, B: Boundedness, R: Retries> From<KeyedStream<K, V, L, B, TotalOrder, R>>
116 for KeyedStream<K, V, L, B, NoOrder, R>
117where
118 L: Location<'a>,
119{
120 fn from(stream: KeyedStream<K, V, L, B, TotalOrder, R>) -> KeyedStream<K, V, L, B, NoOrder, R> {
121 stream.weaken_ordering()
122 }
123}
124
125impl<'a, K, V, L, O: Ordering, R: Retries> DeferTick for KeyedStream<K, V, Tick<L>, Bounded, O, R>
126where
127 L: Location<'a>,
128{
129 fn defer_tick(self) -> Self {
130 KeyedStream::defer_tick(self)
131 }
132}
133
134impl<'a, K, V, L, O: Ordering, R: Retries> CycleCollection<'a, TickCycle>
135 for KeyedStream<K, V, Tick<L>, Bounded, O, R>
136where
137 L: Location<'a>,
138{
139 type Location = Tick<L>;
140
141 fn create_source(cycle_id: CycleId, location: Tick<L>) -> Self {
142 let flow_state = location.flow_state().clone();
143 KeyedStream {
144 ir_node: crate::live_collections::tracked_ir_node(
145 &flow_state,
146 HydroNode::CycleSource {
147 cycle_id,
148 metadata: location.new_node_metadata(
149 KeyedStream::<K, V, Tick<L>, Bounded, O, R>::collection_kind(),
150 ),
151 },
152 ),
153 flow_state,
154 location,
155 _phantom: PhantomData,
156 }
157 }
158}
159
160impl<'a, K, V, L, O: Ordering, R: Retries> ReceiverComplete<'a, TickCycle>
161 for KeyedStream<K, V, Tick<L>, Bounded, O, R>
162where
163 L: Location<'a>,
164{
165 fn complete(self, cycle_id: CycleId, expected_location: LocationId) {
166 assert_eq!(
167 Location::id(&self.location),
168 expected_location,
169 "locations do not match"
170 );
171
172 self.location
173 .flow_state()
174 .borrow_mut()
175 .push_root(HydroRoot::CycleSink {
176 cycle_id,
177 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
178 op_metadata: HydroIrOpMetadata::new(),
179 });
180 }
181}
182
183impl<'a, K, V, L, B: Boundedness, O: Ordering, R: Retries> CycleCollection<'a, ForwardRef>
184 for KeyedStream<K, V, L, B, O, R>
185where
186 L: Location<'a>,
187{
188 type Location = L;
189
190 fn create_source(cycle_id: CycleId, location: L) -> Self {
191 let flow_state = location.flow_state().clone();
192 KeyedStream {
193 ir_node: crate::live_collections::tracked_ir_node(
194 &flow_state,
195 HydroNode::CycleSource {
196 cycle_id,
197 metadata: location
198 .new_node_metadata(KeyedStream::<K, V, L, B, O, R>::collection_kind()),
199 },
200 ),
201 flow_state,
202 location,
203 _phantom: PhantomData,
204 }
205 }
206}
207
208impl<'a, K, V, L, B: Boundedness, O: Ordering, R: Retries> ReceiverComplete<'a, ForwardRef>
209 for KeyedStream<K, V, L, B, O, R>
210where
211 L: Location<'a>,
212{
213 fn complete(self, cycle_id: CycleId, expected_location: LocationId) {
214 assert_eq!(
215 Location::id(&self.location),
216 expected_location,
217 "locations do not match"
218 );
219 self.location
220 .flow_state()
221 .borrow_mut()
222 .push_root(HydroRoot::CycleSink {
223 cycle_id,
224 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
225 op_metadata: HydroIrOpMetadata::new(),
226 });
227 }
228}
229
230impl<'a, K: Clone, V: Clone, Loc: Location<'a>, Bound: Boundedness, Order: Ordering, R: Retries>
231 Clone for KeyedStream<K, V, Loc, Bound, Order, R>
232{
233 fn clone(&self) -> Self {
234 if !matches!(self.ir_node.borrow().deref(), HydroNode::Tee { .. }) {
235 let orig_ir_node = self.ir_node.replace(HydroNode::Placeholder);
236 *self.ir_node.borrow_mut() = HydroNode::Tee {
237 inner: SharedNode(Rc::new(RefCell::new(orig_ir_node))),
238 metadata: self.location.new_node_metadata(Self::collection_kind()),
239 };
240 }
241
242 if let HydroNode::Tee { inner, metadata } = self.ir_node.borrow().deref() {
243 KeyedStream {
244 location: self.location.clone(),
245 flow_state: self.flow_state.clone(),
246 ir_node: crate::live_collections::tracked_ir_node(
247 &self.flow_state,
248 HydroNode::Tee {
249 inner: SharedNode(inner.0.clone()),
250 metadata: metadata.clone(),
251 },
252 ),
253 _phantom: PhantomData,
254 }
255 } else {
256 unreachable!()
257 }
258 }
259}
260
261/// The output of a Hydro generator created with [`KeyedStream::generator`], which can yield elements and
262/// control the processing of future elements.
263pub enum Generate<T> {
264 /// Emit the provided element, and keep processing future inputs.
265 Yield(T),
266 /// Emit the provided element as the _final_ element, do not process future inputs.
267 Return(T),
268 /// Do not emit anything, but continue processing future inputs.
269 Continue,
270 /// Do not emit anything, and do not process further inputs.
271 Break,
272}
273
274impl<'a, K, V, L: Location<'a>, B: Boundedness, O: Ordering, R: Retries>
275 KeyedStream<K, V, L, B, O, R>
276{
277 pub(crate) fn new(location: L, ir_node: HydroNode) -> Self {
278 debug_assert_eq!(ir_node.metadata().location_id, Location::id(&location));
279 debug_assert_eq!(ir_node.metadata().collection_kind, Self::collection_kind());
280
281 let flow_state = location.flow_state().clone();
282 let ir_node = crate::live_collections::tracked_ir_node(&flow_state, ir_node);
283 KeyedStream {
284 location,
285 flow_state,
286 ir_node,
287 _phantom: PhantomData,
288 }
289 }
290
291 /// Returns the [`CollectionKind`] corresponding to this type.
292 pub fn collection_kind() -> CollectionKind {
293 CollectionKind::KeyedStream {
294 bound: B::BOUND_KIND,
295 value_order: O::ORDERING_KIND,
296 value_retry: R::RETRIES_KIND,
297 key_type: stageleft::quote_type::<K>().into(),
298 value_type: stageleft::quote_type::<V>().into(),
299 }
300 }
301
302 /// Returns the [`Location`] where this keyed stream is being materialized.
303 pub fn location(&self) -> &L {
304 &self.location
305 }
306
307 /// Weakens the consistency of this live collection to not guarantee any consistency across
308 /// cluster members (if this collection is on a cluster).
309 pub fn weaken_consistency(self) -> KeyedStream<K, V, L::DropConsistency, B, O, R>
310 where
311 L: Location<'a>,
312 {
313 if L::consistency()
314 .is_none_or(|c| c == crate::location::dynamic::ClusterConsistency::NoConsistency)
315 {
316 // already no consistency
317 KeyedStream::new(
318 self.location.drop_consistency(),
319 self.ir_node.replace(HydroNode::Placeholder),
320 )
321 } else {
322 KeyedStream::new(
323 self.location.drop_consistency(),
324 HydroNode::Cast {
325 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
326 metadata: self
327 .location
328 .drop_consistency()
329 .new_node_metadata(
330 KeyedStream::<K, V, L::DropConsistency, B>::collection_kind(),
331 ),
332 },
333 )
334 }
335 }
336
337 /// Casts this live collection to have the consistency guarantees specified in the given
338 /// location type parameter. The developer must ensure that the strengthened consistency
339 /// is actually guaranteed, via the proof field (see [`crate::prelude::manual_proof`]).
340 pub fn assert_has_consistency_of<L2: Location<'a, DropConsistency = L::DropConsistency>>(
341 self,
342 _proof: impl crate::properties::ConsistencyProof,
343 ) -> KeyedStream<K, V, L2, B, O, R>
344 where
345 L: Location<'a>,
346 {
347 if L::consistency() == L2::consistency() {
348 KeyedStream::new(
349 self.location.with_consistency_of(),
350 self.ir_node.replace(HydroNode::Placeholder),
351 )
352 } else {
353 KeyedStream::new(
354 self.location.with_consistency_of(),
355 HydroNode::AssertIsConsistent {
356 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
357 trusted: false,
358 metadata: self
359 .location
360 .clone()
361 .with_consistency_of::<L2>()
362 .new_node_metadata(KeyedStream::<K, V, L2, B, O, R>::collection_kind()),
363 },
364 )
365 }
366 }
367
368 pub(crate) fn assert_has_consistency_of_trusted<
369 L2: Location<'a, DropConsistency = L::DropConsistency>,
370 >(
371 self,
372 _proof: impl crate::properties::ConsistencyProof,
373 ) -> KeyedStream<K, V, L2, B, O, R>
374 where
375 L: Location<'a>,
376 {
377 if L::consistency() == L2::consistency() {
378 KeyedStream::new(
379 self.location.with_consistency_of(),
380 self.ir_node.replace(HydroNode::Placeholder),
381 )
382 } else {
383 KeyedStream::new(
384 self.location.with_consistency_of(),
385 HydroNode::AssertIsConsistent {
386 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
387 trusted: true,
388 metadata: self
389 .location
390 .clone()
391 .with_consistency_of::<L2>()
392 .new_node_metadata(KeyedStream::<K, V, L2, B, O, R>::collection_kind()),
393 },
394 )
395 }
396 }
397
398 /// Turns this [`KeyedStream`] into a [`Stream`] preserving ordering, under the invariant
399 /// assumption that there is at most one key. If this invariant is broken, the program
400 /// may exhibit undefined behavior, so uses must be carefully vetted.
401 pub(crate) fn cast_at_most_one_key(self) -> Stream<(K, V), L, B, O, R> {
402 Stream::new(
403 self.location.clone(),
404 HydroNode::Cast {
405 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
406 metadata: self
407 .location
408 .new_node_metadata(Stream::<(K, V), L, B, O, R>::collection_kind()),
409 },
410 )
411 }
412
413 /// Turns this [`KeyedStream`] into a [`KeyedSingleton`], under the invariant assumption that
414 /// there is at most one entry per key. If this invariant is broken, the program may exhibit
415 /// undefined behavior, so uses must be carefully vetted.
416 pub(crate) fn cast_at_most_one_entry_per_key(
417 self,
418 ) -> KeyedSingleton<K, V, L, B::WithBoundedValue> {
419 KeyedSingleton::new(
420 self.location.clone(),
421 HydroNode::Cast {
422 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
423 metadata: self.location.new_node_metadata(KeyedSingleton::<
424 K,
425 V,
426 L,
427 B::WithBoundedValue,
428 >::collection_kind()),
429 },
430 )
431 }
432
433 pub(crate) fn use_ordering_type<O2: Ordering>(self) -> KeyedStream<K, V, L, B, O2, R> {
434 if O::ORDERING_KIND == O2::ORDERING_KIND {
435 KeyedStream::new(
436 self.location.clone(),
437 self.ir_node.replace(HydroNode::Placeholder),
438 )
439 } else {
440 panic!(
441 "Runtime ordering {:?} did not match requested cast {:?}.",
442 O::ORDERING_KIND,
443 O2::ORDERING_KIND
444 )
445 }
446 }
447
448 /// Explicitly "casts" the keyed stream to a type with a different ordering
449 /// guarantee for each group. Useful in unsafe code where the ordering cannot be proven
450 /// by the type-system.
451 ///
452 /// # Non-Determinism
453 /// This function is used as an escape hatch, and any mistakes in the
454 /// provided ordering guarantee will propagate into the guarantees
455 /// for the rest of the program.
456 pub fn assume_ordering<O2: Ordering>(
457 self,
458 _nondet: NonDet,
459 ) -> KeyedStream<K, V, L::DropConsistency, B, O2, R> {
460 if O::ORDERING_KIND == O2::ORDERING_KIND {
461 self.use_ordering_type().weaken_consistency()
462 } else if O2::ORDERING_KIND == StreamOrder::NoOrder {
463 // We can always weaken the ordering guarantee
464 let target_location = self.location.drop_consistency();
465 KeyedStream::new(
466 target_location.clone(),
467 HydroNode::Cast {
468 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
469 metadata: target_location
470 .new_node_metadata(KeyedStream::<K, V, L, B, O2, R>::collection_kind()),
471 },
472 )
473 } else {
474 let target_location = self.location.drop_consistency();
475 KeyedStream::new(
476 target_location.clone(),
477 HydroNode::ObserveNonDet {
478 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
479 trusted: false,
480 metadata: target_location
481 .new_node_metadata(KeyedStream::<K, V, L, B, O2, R>::collection_kind()),
482 },
483 )
484 }
485 }
486
487 fn assume_ordering_trusted<O2: Ordering>(
488 self,
489 _nondet: NonDet,
490 ) -> KeyedStream<K, V, L, B, O2, R> {
491 if O::ORDERING_KIND == O2::ORDERING_KIND {
492 KeyedStream::new(
493 self.location.clone(),
494 self.ir_node.replace(HydroNode::Placeholder),
495 )
496 } else if O2::ORDERING_KIND == StreamOrder::NoOrder {
497 // We can always weaken the ordering guarantee
498 KeyedStream::new(
499 self.location.clone(),
500 HydroNode::Cast {
501 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
502 metadata: self
503 .location
504 .new_node_metadata(KeyedStream::<K, V, L, B, O2, R>::collection_kind()),
505 },
506 )
507 } else {
508 KeyedStream::new(
509 self.location.clone(),
510 HydroNode::ObserveNonDet {
511 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
512 trusted: true,
513 metadata: self
514 .location
515 .new_node_metadata(KeyedStream::<K, V, L, B, O2, R>::collection_kind()),
516 },
517 )
518 }
519 }
520
521 #[deprecated = "use `weaken_ordering::<NoOrder>()` instead"]
522 /// Weakens the ordering guarantee provided by the stream to [`NoOrder`],
523 /// which is always safe because that is the weakest possible guarantee.
524 pub fn weakest_ordering(self) -> KeyedStream<K, V, L, B, NoOrder, R> {
525 self.weaken_ordering::<NoOrder>()
526 }
527
528 /// Weakens the ordering guarantee provided by the stream to `O2`, with the type-system
529 /// enforcing that `O2` is weaker than the input ordering guarantee.
530 pub fn weaken_ordering<O2: WeakerOrderingThan<O>>(self) -> KeyedStream<K, V, L, B, O2, R> {
531 let nondet = nondet!(/** this is a weaker ordering guarantee, so it is safe to assume */);
532 self.assume_ordering_trusted::<O2>(nondet)
533 }
534
535 /// Strengthens the ordering guarantee to `TotalOrder`, given that `O: IsOrdered`, which
536 /// implies that `O == TotalOrder`.
537 pub fn make_totally_ordered(self) -> KeyedStream<K, V, L, B, TotalOrder, R>
538 where
539 O: IsOrdered,
540 {
541 self.assume_ordering_trusted(nondet!(/** no-op */))
542 }
543
544 /// Explicitly "casts" the keyed stream to a type with a different retries
545 /// guarantee for each group. Useful in unsafe code where the lack of retries cannot
546 /// be proven by the type-system.
547 ///
548 /// # Non-Determinism
549 /// This function is used as an escape hatch, and any mistakes in the
550 /// provided retries guarantee will propagate into the guarantees
551 /// for the rest of the program.
552 pub fn assume_retries<R2: Retries>(
553 self,
554 _nondet: NonDet,
555 ) -> KeyedStream<K, V, L::DropConsistency, B, O, R2> {
556 if R::RETRIES_KIND == R2::RETRIES_KIND {
557 KeyedStream::new(
558 self.location.drop_consistency(),
559 self.ir_node.replace(HydroNode::Placeholder),
560 )
561 } else if R2::RETRIES_KIND == StreamRetry::AtLeastOnce {
562 // We can always weaken the retries guarantee
563 let target_location = self.location.drop_consistency();
564 KeyedStream::new(
565 target_location.clone(),
566 HydroNode::Cast {
567 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
568 metadata: target_location
569 .new_node_metadata(KeyedStream::<K, V, L, B, O, R2>::collection_kind()),
570 },
571 )
572 } else {
573 let target_location = self.location.drop_consistency();
574 KeyedStream::new(
575 target_location.clone(),
576 HydroNode::ObserveNonDet {
577 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
578 trusted: false,
579 metadata: target_location
580 .new_node_metadata(KeyedStream::<K, V, L, B, O, R2>::collection_kind()),
581 },
582 )
583 }
584 }
585
586 // only for internal APIs that have been carefully vetted to ensure that the non-determinism
587 // is not observable
588 fn assume_retries_trusted<R2: Retries>(
589 self,
590 _nondet: NonDet,
591 ) -> KeyedStream<K, V, L, B, O, R2> {
592 if R::RETRIES_KIND == R2::RETRIES_KIND {
593 KeyedStream::new(
594 self.location.clone(),
595 self.ir_node.replace(HydroNode::Placeholder),
596 )
597 } else if R2::RETRIES_KIND == StreamRetry::AtLeastOnce {
598 // We can always weaken the retries guarantee
599 KeyedStream::new(
600 self.location.clone(),
601 HydroNode::Cast {
602 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
603 metadata: self
604 .location
605 .new_node_metadata(KeyedStream::<K, V, L, B, O, R2>::collection_kind()),
606 },
607 )
608 } else {
609 KeyedStream::new(
610 self.location.clone(),
611 HydroNode::ObserveNonDet {
612 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
613 trusted: true,
614 metadata: self
615 .location
616 .new_node_metadata(KeyedStream::<K, V, L, B, O, R2>::collection_kind()),
617 },
618 )
619 }
620 }
621
622 #[deprecated = "use `weaken_retries::<AtLeastOnce>()` instead"]
623 /// Weakens the retries guarantee provided by the stream to [`AtLeastOnce`],
624 /// which is always safe because that is the weakest possible guarantee.
625 pub fn weakest_retries(self) -> KeyedStream<K, V, L, B, O, AtLeastOnce> {
626 self.weaken_retries::<AtLeastOnce>()
627 }
628
629 /// Weakens the retries guarantee provided by the stream to `R2`, with the type-system
630 /// enforcing that `R2` is weaker than the input retries guarantee.
631 pub fn weaken_retries<R2: WeakerRetryThan<R>>(self) -> KeyedStream<K, V, L, B, O, R2> {
632 let nondet = nondet!(/** this is a weaker retries guarantee, so it is safe to assume */);
633 self.assume_retries_trusted::<R2>(nondet)
634 }
635
636 /// Strengthens the retry guarantee to `ExactlyOnce`, given that `R: IsExactlyOnce`, which
637 /// implies that `R == ExactlyOnce`.
638 pub fn make_exactly_once(self) -> KeyedStream<K, V, L, B, O, ExactlyOnce>
639 where
640 R: IsExactlyOnce,
641 {
642 self.assume_retries_trusted(nondet!(/** no-op */))
643 }
644
645 /// Strengthens the boundedness guarantee to `Bounded`, given that `B: IsBounded`, which
646 /// implies that `B == Bounded`.
647 pub fn make_bounded(self) -> KeyedStream<K, V, L, Bounded, O, R>
648 where
649 B: IsBounded,
650 {
651 self.weaken_boundedness()
652 }
653
654 /// Weakens the boundedness guarantee to an arbitrary boundedness `B2`, given that `B: IsBounded`,
655 /// which implies that `B == Bounded`.
656 pub fn weaken_boundedness<B2: Boundedness>(self) -> KeyedStream<K, V, L, B2, O, R> {
657 if B::BOUNDED == B2::BOUNDED {
658 KeyedStream::new(
659 self.location.clone(),
660 self.ir_node.replace(HydroNode::Placeholder),
661 )
662 } else {
663 // We can always weaken the boundedness
664 KeyedStream::new(
665 self.location.clone(),
666 HydroNode::Cast {
667 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
668 metadata: self
669 .location
670 .new_node_metadata(KeyedStream::<K, V, L, B2, O, R>::collection_kind()),
671 },
672 )
673 }
674 }
675
676 /// Flattens the keyed stream into an unordered stream of key-value pairs.
677 ///
678 /// # Example
679 /// ```rust
680 /// # #[cfg(feature = "deploy")] {
681 /// # use hydro_lang::prelude::*;
682 /// # use futures::StreamExt;
683 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
684 /// process
685 /// .source_iter(q!(vec![(1, 2), (1, 3), (2, 4)]))
686 /// .into_keyed()
687 /// .entries()
688 /// # }, |mut stream| async move {
689 /// // (1, 2), (1, 3), (2, 4) in any order
690 /// # let mut results = Vec::new();
691 /// # for _ in 0..3 {
692 /// # results.push(stream.next().await.unwrap());
693 /// # }
694 /// # results.sort();
695 /// # assert_eq!(results, vec![(1, 2), (1, 3), (2, 4)]);
696 /// # }));
697 /// # }
698 /// ```
699 pub fn entries(self) -> Stream<(K, V), L, B, NoOrder, R> {
700 Stream::new(
701 self.location.clone(),
702 HydroNode::Cast {
703 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
704 metadata: self
705 .location
706 .new_node_metadata(Stream::<(K, V), L, B, NoOrder, R>::collection_kind()),
707 },
708 )
709 }
710
711 /// Flattens the keyed stream into a totally ordered stream of key-value pairs,
712 /// preserving the order of values within each key group but non-deterministically
713 /// interleaving across keys.
714 ///
715 /// Requires the keyed stream to be totally ordered within each group (`O: IsOrdered`).
716 ///
717 /// # Non-Determinism
718 /// The interleaving of entries across different keys is non-deterministic.
719 /// Within each key, the original order is preserved.
720 pub fn entries_partially_ordered(
721 self,
722 _nondet: NonDet,
723 ) -> Stream<(K, V), L::DropConsistency, B, TotalOrder, R>
724 where
725 O: IsOrdered,
726 {
727 let target_location = self.location.drop_consistency();
728 Stream::new(
729 target_location.clone(),
730 HydroNode::ObserveNonDet {
731 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
732 trusted: false,
733 metadata: target_location
734 .new_node_metadata(Stream::<(K, V), L, B, TotalOrder, R>::collection_kind()),
735 },
736 )
737 }
738
739 /// Flattens the keyed stream into an unordered stream of only the values.
740 ///
741 /// # Example
742 /// ```rust
743 /// # #[cfg(feature = "deploy")] {
744 /// # use hydro_lang::prelude::*;
745 /// # use futures::StreamExt;
746 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
747 /// process
748 /// .source_iter(q!(vec![(1, 2), (1, 3), (2, 4)]))
749 /// .into_keyed()
750 /// .values()
751 /// # }, |mut stream| async move {
752 /// // 2, 3, 4 in any order
753 /// # let mut results = Vec::new();
754 /// # for _ in 0..3 {
755 /// # results.push(stream.next().await.unwrap());
756 /// # }
757 /// # results.sort();
758 /// # assert_eq!(results, vec![2, 3, 4]);
759 /// # }));
760 /// # }
761 /// ```
762 pub fn values(self) -> Stream<V, L, B, NoOrder, R> {
763 self.entries().map(q!(|(_, v)| v))
764 }
765
766 /// Flattens the keyed stream into an unordered stream of just the keys.
767 ///
768 /// # Example
769 /// ```rust
770 /// # #[cfg(feature = "deploy")] {
771 /// # use hydro_lang::prelude::*;
772 /// # use futures::StreamExt;
773 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
774 /// # process
775 /// # .source_iter(q!(vec![(1, 2), (2, 4), (1, 5)]))
776 /// # .into_keyed()
777 /// # .keys()
778 /// # }, |mut stream| async move {
779 /// // 1, 2 in any order
780 /// # let mut results = Vec::new();
781 /// # for _ in 0..2 {
782 /// # results.push(stream.next().await.unwrap());
783 /// # }
784 /// # results.sort();
785 /// # assert_eq!(results, vec![1, 2]);
786 /// # }));
787 /// # }
788 /// ```
789 pub fn keys(self) -> Stream<K, L, B, NoOrder, ExactlyOnce>
790 where
791 K: Eq + Hash,
792 {
793 self.entries().map(q!(|(k, _)| k)).unique()
794 }
795
796 /// Transforms each value by invoking `f` on each element, with keys staying the same
797 /// after transformation. If you need access to the key, see [`KeyedStream::map_with_key`].
798 ///
799 /// If you do not want to modify the stream and instead only want to view
800 /// each item use [`KeyedStream::inspect`] instead.
801 ///
802 /// # Example
803 /// ```rust
804 /// # #[cfg(feature = "deploy")] {
805 /// # use hydro_lang::prelude::*;
806 /// # use futures::StreamExt;
807 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
808 /// process
809 /// .source_iter(q!(vec![(1, 2), (1, 3), (2, 4)]))
810 /// .into_keyed()
811 /// .map(q!(|v| v + 1))
812 /// # .entries()
813 /// # }, |mut stream| async move {
814 /// // { 1: [3, 4], 2: [5] }
815 /// # let mut results = Vec::new();
816 /// # for _ in 0..3 {
817 /// # results.push(stream.next().await.unwrap());
818 /// # }
819 /// # results.sort();
820 /// # assert_eq!(results, vec![(1, 3), (1, 4), (2, 5)]);
821 /// # }));
822 /// # }
823 /// ```
824 pub fn map<U, F>(
825 self,
826 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>> + Copy,
827 ) -> KeyedStream<K, U, L, B, O, R>
828 where
829 F: Fn(V) -> U + 'a,
830 {
831 let f: ManualExpr<F, _> =
832 ManualExpr::new(move |ctx: &OperatorContext<L, B>| f.splice_fn1_ctx(ctx));
833 let map_f = q!({
834 let orig = f;
835 move |(k, v)| (k, orig(v))
836 })
837 .splice_fn1_ctx::<(K, V), (K, U)>(&OperatorContext::<L, B>::new(&self.location))
838 .into();
839
840 KeyedStream::new(
841 self.location.clone(),
842 HydroNode::Map {
843 f: map_f,
844 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
845 metadata: self
846 .location
847 .new_node_metadata(KeyedStream::<K, U, L, B, O, R>::collection_kind()),
848 },
849 )
850 }
851
852 /// Transforms each value by invoking `f` on each key-value pair. The resulting values are **not**
853 /// re-grouped even they are tuples; instead they will be grouped under the original key.
854 ///
855 /// If you do not want to modify the stream and instead only want to view
856 /// each item use [`KeyedStream::inspect_with_key`] instead.
857 ///
858 /// # Example
859 /// ```rust
860 /// # #[cfg(feature = "deploy")] {
861 /// # use hydro_lang::prelude::*;
862 /// # use futures::StreamExt;
863 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
864 /// process
865 /// .source_iter(q!(vec![(1, 2), (1, 3), (2, 4)]))
866 /// .into_keyed()
867 /// .map_with_key(q!(|(k, v)| k + v))
868 /// # .entries()
869 /// # }, |mut stream| async move {
870 /// // { 1: [3, 4], 2: [6] }
871 /// # let mut results = Vec::new();
872 /// # for _ in 0..3 {
873 /// # results.push(stream.next().await.unwrap());
874 /// # }
875 /// # results.sort();
876 /// # assert_eq!(results, vec![(1, 3), (1, 4), (2, 6)]);
877 /// # }));
878 /// # }
879 /// ```
880 pub fn map_with_key<U, F>(
881 self,
882 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>> + Copy,
883 ) -> KeyedStream<K, U, L, B, O, R>
884 where
885 F: Fn((K, V)) -> U + 'a,
886 K: Clone,
887 {
888 let f: ManualExpr<F, _> =
889 ManualExpr::new(move |ctx: &OperatorContext<L, B>| f.splice_fn1_ctx(ctx));
890 let map_f = q!({
891 let orig = f;
892 move |(k, v)| {
893 let out = orig((Clone::clone(&k), v));
894 (k, out)
895 }
896 })
897 .splice_fn1_ctx::<(K, V), (K, U)>(&OperatorContext::<L, B>::new(&self.location))
898 .into();
899
900 KeyedStream::new(
901 self.location.clone(),
902 HydroNode::Map {
903 f: map_f,
904 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
905 metadata: self
906 .location
907 .new_node_metadata(KeyedStream::<K, U, L, B, O, R>::collection_kind()),
908 },
909 )
910 }
911
912 /// Prepends a new value to the key of each element in the stream, producing a new
913 /// keyed stream with compound keys. Because the original key is preserved, no re-grouping
914 /// occurs and the elements in each group preserve their original order.
915 ///
916 /// # Example
917 /// ```rust
918 /// # #[cfg(feature = "deploy")] {
919 /// # use hydro_lang::prelude::*;
920 /// # use futures::StreamExt;
921 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
922 /// process
923 /// .source_iter(q!(vec![(1, 2), (1, 3), (2, 4)]))
924 /// .into_keyed()
925 /// .prefix_key(q!(|&(k, _)| k % 2))
926 /// # .entries()
927 /// # }, |mut stream| async move {
928 /// // { (1, 1): [2, 3], (0, 2): [4] }
929 /// # let mut results = Vec::new();
930 /// # for _ in 0..3 {
931 /// # results.push(stream.next().await.unwrap());
932 /// # }
933 /// # results.sort();
934 /// # assert_eq!(results, vec![((0, 2), 4), ((1, 1), 2), ((1, 1), 3)]);
935 /// # }));
936 /// # }
937 /// ```
938 pub fn prefix_key<K2, F>(
939 self,
940 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>> + Copy,
941 ) -> KeyedStream<(K2, K), V, L, B, O, R>
942 where
943 F: Fn(&(K, V)) -> K2 + 'a,
944 {
945 let f: ManualExpr<F, _> =
946 ManualExpr::new(move |ctx: &OperatorContext<L, B>| f.splice_fn1_borrow_ctx(ctx));
947 let map_f = q!({
948 let orig = f;
949 move |kv| {
950 let out = orig(&kv);
951 ((out, kv.0), kv.1)
952 }
953 })
954 .splice_fn1_ctx::<(K, V), ((K2, K), V)>(&OperatorContext::<L, B>::new(&self.location))
955 .into();
956
957 KeyedStream::new(
958 self.location.clone(),
959 HydroNode::Map {
960 f: map_f,
961 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
962 metadata: self
963 .location
964 .new_node_metadata(KeyedStream::<(K2, K), V, L, B, O, R>::collection_kind()),
965 },
966 )
967 }
968
969 /// Creates a stream containing only the elements of each group stream that satisfy a predicate
970 /// `f`, preserving the order of the elements within the group.
971 ///
972 /// The closure `f` receives a reference `&V` rather than an owned value `v` because filtering does
973 /// not modify or take ownership of the values. If you need to modify the values while filtering
974 /// use [`KeyedStream::filter_map`] instead.
975 ///
976 /// # Example
977 /// ```rust
978 /// # #[cfg(feature = "deploy")] {
979 /// # use hydro_lang::prelude::*;
980 /// # use futures::StreamExt;
981 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
982 /// process
983 /// .source_iter(q!(vec![(1, 2), (1, 3), (2, 4)]))
984 /// .into_keyed()
985 /// .filter(q!(|&x| x > 2))
986 /// # .entries()
987 /// # }, |mut stream| async move {
988 /// // { 1: [3], 2: [4] }
989 /// # let mut results = Vec::new();
990 /// # for _ in 0..2 {
991 /// # results.push(stream.next().await.unwrap());
992 /// # }
993 /// # results.sort();
994 /// # assert_eq!(results, vec![(1, 3), (2, 4)]);
995 /// # }));
996 /// # }
997 /// ```
998 pub fn filter<F>(
999 self,
1000 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>> + Copy,
1001 ) -> KeyedStream<K, V, L, B, O, R>
1002 where
1003 F: Fn(&V) -> bool + 'a,
1004 {
1005 let f: ManualExpr<F, _> =
1006 ManualExpr::new(move |ctx: &OperatorContext<L, B>| f.splice_fn1_borrow_ctx(ctx));
1007 let filter_f = q!({
1008 let orig = f;
1009 move |t: &(_, _)| orig(&t.1)
1010 })
1011 .splice_fn1_borrow_ctx::<(K, V), bool>(&OperatorContext::<L, B>::new(&self.location))
1012 .into();
1013
1014 KeyedStream::new(
1015 self.location.clone(),
1016 HydroNode::Filter {
1017 f: filter_f,
1018 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1019 metadata: self.location.new_node_metadata(Self::collection_kind()),
1020 },
1021 )
1022 }
1023
1024 /// Creates a stream containing only the elements of each group stream that satisfy a predicate
1025 /// `f` (which receives the key-value tuple), preserving the order of the elements within the group.
1026 ///
1027 /// The closure `f` receives a reference `&(K, V)` rather than an owned value `(K, V)` because filtering does
1028 /// not modify or take ownership of the values. If you need to modify the values while filtering
1029 /// use [`KeyedStream::filter_map_with_key`] instead.
1030 ///
1031 /// # Example
1032 /// ```rust
1033 /// # #[cfg(feature = "deploy")] {
1034 /// # use hydro_lang::prelude::*;
1035 /// # use futures::StreamExt;
1036 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1037 /// process
1038 /// .source_iter(q!(vec![(1, 2), (1, 3), (2, 4)]))
1039 /// .into_keyed()
1040 /// .filter_with_key(q!(|&(k, v)| v - k == 2))
1041 /// # .entries()
1042 /// # }, |mut stream| async move {
1043 /// // { 1: [3], 2: [4] }
1044 /// # let mut results = Vec::new();
1045 /// # for _ in 0..2 {
1046 /// # results.push(stream.next().await.unwrap());
1047 /// # }
1048 /// # results.sort();
1049 /// # assert_eq!(results, vec![(1, 3), (2, 4)]);
1050 /// # }));
1051 /// # }
1052 /// ```
1053 pub fn filter_with_key<F>(
1054 self,
1055 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>> + Copy,
1056 ) -> KeyedStream<K, V, L, B, O, R>
1057 where
1058 F: Fn(&(K, V)) -> bool + 'a,
1059 {
1060 let filter_f = f
1061 .splice_fn1_borrow_ctx::<(K, V), bool>(&OperatorContext::<L, B>::new(&self.location))
1062 .into();
1063
1064 KeyedStream::new(
1065 self.location.clone(),
1066 HydroNode::Filter {
1067 f: filter_f,
1068 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1069 metadata: self.location.new_node_metadata(Self::collection_kind()),
1070 },
1071 )
1072 }
1073
1074 /// An operator that both filters and maps each value, with keys staying the same.
1075 /// It yields only the items for which the supplied closure `f` returns `Some(value)`.
1076 /// If you need access to the key, see [`KeyedStream::filter_map_with_key`].
1077 ///
1078 /// # Example
1079 /// ```rust
1080 /// # #[cfg(feature = "deploy")] {
1081 /// # use hydro_lang::prelude::*;
1082 /// # use futures::StreamExt;
1083 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1084 /// process
1085 /// .source_iter(q!(vec![(1, "2"), (1, "hello"), (2, "4")]))
1086 /// .into_keyed()
1087 /// .filter_map(q!(|s| s.parse::<usize>().ok()))
1088 /// # .entries()
1089 /// # }, |mut stream| async move {
1090 /// // { 1: [2], 2: [4] }
1091 /// # let mut results = Vec::new();
1092 /// # for _ in 0..2 {
1093 /// # results.push(stream.next().await.unwrap());
1094 /// # }
1095 /// # results.sort();
1096 /// # assert_eq!(results, vec![(1, 2), (2, 4)]);
1097 /// # }));
1098 /// # }
1099 /// ```
1100 pub fn filter_map<U, F>(
1101 self,
1102 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>> + Copy,
1103 ) -> KeyedStream<K, U, L, B, O, R>
1104 where
1105 F: Fn(V) -> Option<U> + 'a,
1106 {
1107 let f: ManualExpr<F, _> =
1108 ManualExpr::new(move |ctx: &OperatorContext<L, B>| f.splice_fn1_ctx(ctx));
1109 let filter_map_f = q!({
1110 let orig = f;
1111 move |(k, v)| orig(v).map(|o| (k, o))
1112 })
1113 .splice_fn1_ctx::<(K, V), Option<(K, U)>>(&OperatorContext::<L, B>::new(&self.location))
1114 .into();
1115
1116 KeyedStream::new(
1117 self.location.clone(),
1118 HydroNode::FilterMap {
1119 f: filter_map_f,
1120 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1121 metadata: self
1122 .location
1123 .new_node_metadata(KeyedStream::<K, U, L, B, O, R>::collection_kind()),
1124 },
1125 )
1126 }
1127
1128 /// An operator that both filters and maps each key-value pair. The resulting values are **not**
1129 /// re-grouped even they are tuples; instead they will be grouped under the original key.
1130 /// It yields only the items for which the supplied closure `f` returns `Some(value)`.
1131 ///
1132 /// # Example
1133 /// ```rust
1134 /// # #[cfg(feature = "deploy")] {
1135 /// # use hydro_lang::prelude::*;
1136 /// # use futures::StreamExt;
1137 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1138 /// process
1139 /// .source_iter(q!(vec![(1, "2"), (1, "hello"), (2, "2")]))
1140 /// .into_keyed()
1141 /// .filter_map_with_key(q!(|(k, s)| s.parse::<usize>().ok().filter(|v| v == &k)))
1142 /// # .entries()
1143 /// # }, |mut stream| async move {
1144 /// // { 2: [2] }
1145 /// # let mut results = Vec::new();
1146 /// # for _ in 0..1 {
1147 /// # results.push(stream.next().await.unwrap());
1148 /// # }
1149 /// # results.sort();
1150 /// # assert_eq!(results, vec![(2, 2)]);
1151 /// # }));
1152 /// # }
1153 /// ```
1154 pub fn filter_map_with_key<U, F>(
1155 self,
1156 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>> + Copy,
1157 ) -> KeyedStream<K, U, L, B, O, R>
1158 where
1159 F: Fn((K, V)) -> Option<U> + 'a,
1160 K: Clone,
1161 {
1162 let f: ManualExpr<F, _> =
1163 ManualExpr::new(move |ctx: &OperatorContext<L, B>| f.splice_fn1_ctx(ctx));
1164 let filter_map_f = q!({
1165 let orig = f;
1166 move |(k, v)| {
1167 let out = orig((Clone::clone(&k), v));
1168 out.map(|o| (k, o))
1169 }
1170 })
1171 .splice_fn1_ctx::<(K, V), Option<(K, U)>>(&OperatorContext::<L, B>::new(&self.location))
1172 .into();
1173
1174 KeyedStream::new(
1175 self.location.clone(),
1176 HydroNode::FilterMap {
1177 f: filter_map_f,
1178 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1179 metadata: self
1180 .location
1181 .new_node_metadata(KeyedStream::<K, U, L, B, O, R>::collection_kind()),
1182 },
1183 )
1184 }
1185
1186 /// Generates a keyed stream that maps each value `v` to a tuple `(v, x)`,
1187 /// where `v` is the value of `other`, a bounded [`super::singleton::Singleton`] or
1188 /// [`Optional`]. If `other` is an empty [`Optional`], no values will be produced.
1189 ///
1190 /// # Example
1191 /// ```rust
1192 /// # #[cfg(feature = "deploy")] {
1193 /// # use hydro_lang::prelude::*;
1194 /// # use futures::StreamExt;
1195 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1196 /// let tick = process.tick();
1197 /// let batch = process
1198 /// .source_iter(q!(vec![(1, 123), (1, 456), (2, 123)]))
1199 /// .into_keyed()
1200 /// .batch(&tick, nondet!(/** test */));
1201 /// let count = batch.clone().entries().count(); // `count()` returns a singleton
1202 /// batch.cross_singleton(count).all_ticks().entries()
1203 /// # }, |mut stream| async move {
1204 /// // { 1: [(123, 3), (456, 3)], 2: [(123, 3)] }
1205 /// # let mut results = Vec::new();
1206 /// # for _ in 0..3 {
1207 /// # results.push(stream.next().await.unwrap());
1208 /// # }
1209 /// # results.sort();
1210 /// # assert_eq!(results, vec![(1, (123, 3)), (1, (456, 3)), (2, (123, 3))]);
1211 /// # }));
1212 /// # }
1213 /// ```
1214 pub fn cross_singleton<O2>(
1215 self,
1216 other: impl Into<Optional<O2, L, Bounded>>,
1217 ) -> KeyedStream<K, (V, O2), L, B, O, R>
1218 where
1219 O2: Clone,
1220 {
1221 let other: Optional<O2, L, Bounded> = other.into();
1222 check_matching_location(&self.location, &other.location);
1223
1224 Stream::<((K, V), O2), L, B, O, R>::new(
1225 self.location.clone(),
1226 HydroNode::CrossSingleton {
1227 left: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1228 right: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
1229 metadata: self
1230 .location
1231 .new_node_metadata(Stream::<((K, V), O2), L, B, O, R>::collection_kind()),
1232 },
1233 )
1234 .map(q!(|((k, v), o2)| (k, (v, o2))))
1235 .into_keyed()
1236 }
1237
1238 /// For each value `v` in each group, transform `v` using `f` and then treat the
1239 /// result as an [`Iterator`] to produce values one by one within the same group.
1240 /// The implementation for [`Iterator`] for the output type `I` must produce items
1241 /// in a **deterministic** order.
1242 ///
1243 /// For example, `I` could be a `Vec`, but not a `HashSet`. If the order of the items in `I` is
1244 /// not deterministic, use [`KeyedStream::flat_map_unordered`] instead.
1245 ///
1246 /// # Example
1247 /// ```rust
1248 /// # #[cfg(feature = "deploy")] {
1249 /// # use hydro_lang::prelude::*;
1250 /// # use futures::StreamExt;
1251 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1252 /// process
1253 /// .source_iter(q!(vec![(1, vec![2, 3]), (1, vec![4]), (2, vec![5, 6])]))
1254 /// .into_keyed()
1255 /// .flat_map_ordered(q!(|x| x))
1256 /// # .entries()
1257 /// # }, |mut stream| async move {
1258 /// // { 1: [2, 3, 4], 2: [5, 6] }
1259 /// # let mut results = Vec::new();
1260 /// # for _ in 0..5 {
1261 /// # results.push(stream.next().await.unwrap());
1262 /// # }
1263 /// # results.sort();
1264 /// # assert_eq!(results, vec![(1, 2), (1, 3), (1, 4), (2, 5), (2, 6)]);
1265 /// # }));
1266 /// # }
1267 /// ```
1268 pub fn flat_map_ordered<U, I, F>(
1269 self,
1270 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>> + Copy,
1271 ) -> KeyedStream<K, U, L, B, O, R>
1272 where
1273 I: IntoIterator<Item = U>,
1274 F: Fn(V) -> I + 'a,
1275 K: Clone,
1276 {
1277 let f: ManualExpr<F, _> =
1278 ManualExpr::new(move |ctx: &OperatorContext<L, B>| f.splice_fn1_ctx(ctx));
1279 let flat_map_f = q!({
1280 let orig = f;
1281 move |(k, v)| orig(v).into_iter().map(move |u| (Clone::clone(&k), u))
1282 })
1283 .splice_fn1_ctx::<(K, V), _>(&OperatorContext::<L, B>::new(&self.location))
1284 .into();
1285
1286 KeyedStream::new(
1287 self.location.clone(),
1288 HydroNode::FlatMap {
1289 f: flat_map_f,
1290 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1291 metadata: self
1292 .location
1293 .new_node_metadata(KeyedStream::<K, U, L, B, O, R>::collection_kind()),
1294 },
1295 )
1296 }
1297
1298 /// Like [`KeyedStream::flat_map_ordered`], but allows the implementation of [`Iterator`]
1299 /// for the output type `I` to produce items in any order.
1300 ///
1301 /// # Example
1302 /// ```rust
1303 /// # #[cfg(feature = "deploy")] {
1304 /// # use hydro_lang::{prelude::*, live_collections::stream::{NoOrder, ExactlyOnce}};
1305 /// # use futures::StreamExt;
1306 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test::<_, _, _, NoOrder, ExactlyOnce>(|process| {
1307 /// process
1308 /// .source_iter(q!(vec![
1309 /// (1, std::collections::HashSet::<i32>::from_iter(vec![2, 3])),
1310 /// (2, std::collections::HashSet::from_iter(vec![4, 5]))
1311 /// ]))
1312 /// .into_keyed()
1313 /// .flat_map_unordered(q!(|x| x))
1314 /// # .entries()
1315 /// # }, |mut stream| async move {
1316 /// // { 1: [2, 3], 2: [4, 5] } with values in each group in unknown order
1317 /// # let mut results = Vec::new();
1318 /// # for _ in 0..4 {
1319 /// # results.push(stream.next().await.unwrap());
1320 /// # }
1321 /// # results.sort();
1322 /// # assert_eq!(results, vec![(1, 2), (1, 3), (2, 4), (2, 5)]);
1323 /// # }));
1324 /// # }
1325 /// ```
1326 pub fn flat_map_unordered<U, I, F>(
1327 self,
1328 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>> + Copy,
1329 ) -> KeyedStream<K, U, L, B, NoOrder, R>
1330 where
1331 I: IntoIterator<Item = U>,
1332 F: Fn(V) -> I + 'a,
1333 K: Clone,
1334 {
1335 let f: ManualExpr<F, _> =
1336 ManualExpr::new(move |ctx: &OperatorContext<L, B>| f.splice_fn1_ctx(ctx));
1337 let flat_map_f = q!({
1338 let orig = f;
1339 move |(k, v)| orig(v).into_iter().map(move |u| (Clone::clone(&k), u))
1340 })
1341 .splice_fn1_ctx::<(K, V), _>(&OperatorContext::<L, B>::new(&self.location))
1342 .into();
1343
1344 KeyedStream::new(
1345 self.location.clone(),
1346 HydroNode::FlatMap {
1347 f: flat_map_f,
1348 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1349 metadata: self
1350 .location
1351 .new_node_metadata(KeyedStream::<K, U, L, B, NoOrder, R>::collection_kind()),
1352 },
1353 )
1354 }
1355
1356 /// For each value `v` in each group, treat `v` as an [`Iterator`] and produce its items one by one
1357 /// within the same group. The implementation for [`Iterator`] for the value type `V` must produce
1358 /// items in a **deterministic** order.
1359 ///
1360 /// For example, `V` could be a `Vec`, but not a `HashSet`. If the order of the items in `V` is
1361 /// not deterministic, use [`KeyedStream::flatten_unordered`] instead.
1362 ///
1363 /// # Example
1364 /// ```rust
1365 /// # #[cfg(feature = "deploy")] {
1366 /// # use hydro_lang::prelude::*;
1367 /// # use futures::StreamExt;
1368 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1369 /// process
1370 /// .source_iter(q!(vec![(1, vec![2, 3]), (1, vec![4]), (2, vec![5, 6])]))
1371 /// .into_keyed()
1372 /// .flatten_ordered()
1373 /// # .entries()
1374 /// # }, |mut stream| async move {
1375 /// // { 1: [2, 3, 4], 2: [5, 6] }
1376 /// # let mut results = Vec::new();
1377 /// # for _ in 0..5 {
1378 /// # results.push(stream.next().await.unwrap());
1379 /// # }
1380 /// # results.sort();
1381 /// # assert_eq!(results, vec![(1, 2), (1, 3), (1, 4), (2, 5), (2, 6)]);
1382 /// # }));
1383 /// # }
1384 /// ```
1385 pub fn flatten_ordered<U>(self) -> KeyedStream<K, U, L, B, O, R>
1386 where
1387 V: IntoIterator<Item = U>,
1388 K: Clone,
1389 {
1390 self.flat_map_ordered(q!(|d| d))
1391 }
1392
1393 /// Like [`KeyedStream::flatten_ordered`], but allows the implementation of [`Iterator`]
1394 /// for the value type `V` to produce items in any order.
1395 ///
1396 /// # Example
1397 /// ```rust
1398 /// # #[cfg(feature = "deploy")] {
1399 /// # use hydro_lang::{prelude::*, live_collections::stream::{NoOrder, ExactlyOnce}};
1400 /// # use futures::StreamExt;
1401 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test::<_, _, _, NoOrder, ExactlyOnce>(|process| {
1402 /// process
1403 /// .source_iter(q!(vec![
1404 /// (1, std::collections::HashSet::<i32>::from_iter(vec![2, 3])),
1405 /// (2, std::collections::HashSet::from_iter(vec![4, 5]))
1406 /// ]))
1407 /// .into_keyed()
1408 /// .flatten_unordered()
1409 /// # .entries()
1410 /// # }, |mut stream| async move {
1411 /// // { 1: [2, 3], 2: [4, 5] } with values in each group in unknown order
1412 /// # let mut results = Vec::new();
1413 /// # for _ in 0..4 {
1414 /// # results.push(stream.next().await.unwrap());
1415 /// # }
1416 /// # results.sort();
1417 /// # assert_eq!(results, vec![(1, 2), (1, 3), (2, 4), (2, 5)]);
1418 /// # }));
1419 /// # }
1420 /// ```
1421 pub fn flatten_unordered<U>(self) -> KeyedStream<K, U, L, B, NoOrder, R>
1422 where
1423 V: IntoIterator<Item = U>,
1424 K: Clone,
1425 {
1426 self.flat_map_unordered(q!(|d| d))
1427 }
1428
1429 /// An operator which allows you to "inspect" each element of a stream without
1430 /// modifying it. The closure `f` is called on a reference to each value. This is
1431 /// mainly useful for debugging, and should not be used to generate side-effects.
1432 ///
1433 /// # Example
1434 /// ```rust
1435 /// # #[cfg(feature = "deploy")] {
1436 /// # use hydro_lang::prelude::*;
1437 /// # use futures::StreamExt;
1438 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1439 /// process
1440 /// .source_iter(q!(vec![(1, 2), (1, 3), (2, 4)]))
1441 /// .into_keyed()
1442 /// .inspect(q!(|v| println!("{}", v)))
1443 /// # .entries()
1444 /// # }, |mut stream| async move {
1445 /// # let mut results = Vec::new();
1446 /// # for _ in 0..3 {
1447 /// # results.push(stream.next().await.unwrap());
1448 /// # }
1449 /// # results.sort();
1450 /// # assert_eq!(results, vec![(1, 2), (1, 3), (2, 4)]);
1451 /// # }));
1452 /// # }
1453 /// ```
1454 pub fn inspect<F>(self, f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>> + Copy) -> Self
1455 where
1456 F: Fn(&V) + 'a,
1457 {
1458 let f: ManualExpr<F, _> =
1459 ManualExpr::new(move |ctx: &OperatorContext<L, B>| f.splice_fn1_borrow_ctx(ctx));
1460 let inspect_f = q!({
1461 let orig = f;
1462 move |t: &(_, _)| orig(&t.1)
1463 })
1464 .splice_fn1_borrow_ctx::<(K, V), ()>(&OperatorContext::<L, B>::new(&self.location))
1465 .into();
1466
1467 KeyedStream::new(
1468 self.location.clone(),
1469 HydroNode::Inspect {
1470 f: inspect_f,
1471 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1472 metadata: self.location.new_node_metadata(Self::collection_kind()),
1473 },
1474 )
1475 }
1476
1477 /// An operator which allows you to "inspect" each element of a stream without
1478 /// modifying it. The closure `f` is called on a reference to each key-value pair. This is
1479 /// mainly useful for debugging, and should not be used to generate side-effects.
1480 ///
1481 /// # Example
1482 /// ```rust
1483 /// # #[cfg(feature = "deploy")] {
1484 /// # use hydro_lang::prelude::*;
1485 /// # use futures::StreamExt;
1486 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1487 /// process
1488 /// .source_iter(q!(vec![(1, 2), (1, 3), (2, 4)]))
1489 /// .into_keyed()
1490 /// .inspect_with_key(q!(|(k, v)| println!("{}: {}", k, v)))
1491 /// # .entries()
1492 /// # }, |mut stream| async move {
1493 /// # let mut results = Vec::new();
1494 /// # for _ in 0..3 {
1495 /// # results.push(stream.next().await.unwrap());
1496 /// # }
1497 /// # results.sort();
1498 /// # assert_eq!(results, vec![(1, 2), (1, 3), (2, 4)]);
1499 /// # }));
1500 /// # }
1501 /// ```
1502 pub fn inspect_with_key<F>(self, f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>>) -> Self
1503 where
1504 F: Fn(&(K, V)) + 'a,
1505 {
1506 let inspect_f = f
1507 .splice_fn1_borrow_ctx::<(K, V), ()>(&OperatorContext::<L, B>::new(&self.location))
1508 .into();
1509
1510 KeyedStream::new(
1511 self.location.clone(),
1512 HydroNode::Inspect {
1513 f: inspect_f,
1514 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1515 metadata: self.location.new_node_metadata(Self::collection_kind()),
1516 },
1517 )
1518 }
1519
1520 /// An operator which allows you to "name" a `HydroNode`.
1521 /// This is only used for testing, to correlate certain `HydroNode`s with IDs.
1522 pub fn ir_node_named(self, name: &str) -> KeyedStream<K, V, L, B, O, R> {
1523 {
1524 let mut node = self.ir_node.borrow_mut();
1525 let metadata = node.metadata_mut();
1526 metadata.tag = Some(name.to_owned());
1527 }
1528 self
1529 }
1530
1531 /// A special case of [`Stream::scan`] for keyed streams. For each key group the values are transformed via the `f` combinator.
1532 ///
1533 /// Unlike [`KeyedStream::fold`] which only returns the final accumulated value, `scan` produces a new stream
1534 /// containing all intermediate accumulated values paired with the key. The scan operation can also terminate
1535 /// early by returning `None`.
1536 ///
1537 /// The function takes a mutable reference to the accumulator and the current element, and returns
1538 /// an `Option<U>`. If the function returns `Some(value)`, `value` is emitted to the output stream.
1539 /// If the function returns `None`, the stream is terminated and no more elements are processed.
1540 ///
1541 /// The `init` and `f` closures may capture bounded singletons, optionals, or streams by
1542 /// reference via [`by_ref()`](crate::live_collections::Singleton::by_ref), as long as the
1543 /// referenced collection has the same location and boundedness as this stream.
1544 ///
1545 /// # Example
1546 /// ```rust
1547 /// # #[cfg(feature = "deploy")] {
1548 /// # use hydro_lang::prelude::*;
1549 /// # use futures::StreamExt;
1550 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1551 /// process
1552 /// .source_iter(q!(vec![(0, 1), (0, 3), (1, 3), (1, 4)]))
1553 /// .into_keyed()
1554 /// .scan(
1555 /// q!(|| 0),
1556 /// q!(|acc, x| {
1557 /// *acc += x;
1558 /// if *acc % 2 == 0 { None } else { Some(*acc) }
1559 /// }),
1560 /// )
1561 /// # .entries()
1562 /// # }, |mut stream| async move {
1563 /// // Output: { 0: [1], 1: [3, 7] }
1564 /// # let mut results = Vec::new();
1565 /// # for _ in 0..3 {
1566 /// # results.push(stream.next().await.unwrap());
1567 /// # }
1568 /// # results.sort();
1569 /// # assert_eq!(results, vec![(0, 1), (1, 3), (1, 7)]);
1570 /// # }));
1571 /// # }
1572 /// ```
1573 pub fn scan<A, U, I, F>(
1574 self,
1575 init: impl IntoQuotedMut<'a, I, OperatorContext<L, B>> + Copy,
1576 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>> + Copy,
1577 ) -> KeyedStream<K, U, L, B, TotalOrder, ExactlyOnce>
1578 where
1579 O: IsOrdered,
1580 R: IsExactlyOnce,
1581 K: Clone + Eq + Hash,
1582 I: Fn() -> A + 'a,
1583 F: Fn(&mut A, V) -> Option<U> + 'a,
1584 {
1585 let f: ManualExpr<F, _> =
1586 ManualExpr::new(move |ctx: &OperatorContext<L, B>| f.splice_fn2_borrow_mut_ctx(ctx));
1587 self.make_totally_ordered().make_exactly_once().generator(
1588 init,
1589 q!({
1590 let orig = f;
1591 move |state, v| {
1592 if let Some(out) = orig(state, v) {
1593 Generate::Yield(out)
1594 } else {
1595 Generate::Break
1596 }
1597 }
1598 }),
1599 )
1600 }
1601
1602 /// Iteratively processes the elements in each group using a state machine that can yield
1603 /// elements as it processes its inputs. This is designed to mirror the unstable generator
1604 /// syntax in Rust, without requiring special syntax.
1605 ///
1606 /// Like [`KeyedStream::scan`], this function takes in an initializer that emits the initial
1607 /// state for each group. The second argument defines the processing logic, taking in a
1608 /// mutable reference to the group's state and the value to be processed. It emits a
1609 /// [`Generate`] value, whose variants define what is emitted and whether further inputs
1610 /// should be processed.
1611 ///
1612 /// The `init` and `f` closures may capture bounded singletons, optionals, or streams by
1613 /// reference via [`by_ref()`](crate::live_collections::Singleton::by_ref), as long as the
1614 /// referenced collection has the same location and boundedness as this stream.
1615 ///
1616 /// # Example
1617 /// ```rust
1618 /// # #[cfg(feature = "deploy")] {
1619 /// # use hydro_lang::prelude::*;
1620 /// # use futures::StreamExt;
1621 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1622 /// process
1623 /// .source_iter(q!(vec![(0, 1), (0, 3), (0, 100), (0, 10), (1, 3), (1, 4), (1, 3)]))
1624 /// .into_keyed()
1625 /// .generator(
1626 /// q!(|| 0),
1627 /// q!(|acc, x| {
1628 /// *acc += x;
1629 /// if *acc > 100 {
1630 /// hydro_lang::live_collections::keyed_stream::Generate::Return(
1631 /// "done!".to_owned()
1632 /// )
1633 /// } else if *acc % 2 == 0 {
1634 /// hydro_lang::live_collections::keyed_stream::Generate::Yield(
1635 /// "even".to_owned()
1636 /// )
1637 /// } else {
1638 /// hydro_lang::live_collections::keyed_stream::Generate::Continue
1639 /// }
1640 /// }),
1641 /// )
1642 /// # .entries()
1643 /// # }, |mut stream| async move {
1644 /// // Output: { 0: ["even", "done!"], 1: ["even"] }
1645 /// # let mut results = Vec::new();
1646 /// # for _ in 0..3 {
1647 /// # results.push(stream.next().await.unwrap());
1648 /// # }
1649 /// # results.sort();
1650 /// # assert_eq!(results, vec![(0, "done!".to_owned()), (0, "even".to_owned()), (1, "even".to_owned())]);
1651 /// # }));
1652 /// # }
1653 /// ```
1654 pub fn generator<A, U, I, F>(
1655 self,
1656 init: impl IntoQuotedMut<'a, I, OperatorContext<L, B>> + Copy,
1657 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>> + Copy,
1658 ) -> KeyedStream<K, U, L, B, TotalOrder, ExactlyOnce>
1659 where
1660 O: IsOrdered,
1661 R: IsExactlyOnce,
1662 K: Clone + Eq + Hash,
1663 I: Fn() -> A + 'a,
1664 F: Fn(&mut A, V) -> Generate<U> + 'a,
1665 {
1666 let init: ManualExpr<I, _> =
1667 ManualExpr::new(move |ctx: &OperatorContext<L, B>| init.splice_fn0_ctx(ctx));
1668 let f: ManualExpr<F, _> =
1669 ManualExpr::new(move |ctx: &OperatorContext<L, B>| f.splice_fn2_borrow_mut_ctx(ctx));
1670
1671 let this = self.make_totally_ordered().make_exactly_once();
1672
1673 let scan_init = crate::handoff_ref::with_ref_capture(|| {
1674 q!(|| HashMap::new())
1675 .splice_fn0_ctx::<HashMap<K, Option<A>>>(&OperatorContext::<L, B>::new(
1676 &this.location,
1677 ))
1678 .into()
1679 });
1680 let scan_f = crate::handoff_ref::with_ref_capture(|| {
1681 q!(move |acc: &mut HashMap<_, _>, (k, v)| {
1682 let existing_state = acc.entry(Clone::clone(&k)).or_insert_with(|| Some(init()));
1683 if let Some(existing_state_value) = existing_state {
1684 match f(existing_state_value, v) {
1685 Generate::Yield(out) => Some(Some((k, out))),
1686 Generate::Return(out) => {
1687 let _ = existing_state.take(); // TODO(shadaj): garbage collect with termination markers
1688 Some(Some((k, out)))
1689 }
1690 Generate::Break => {
1691 let _ = existing_state.take(); // TODO(shadaj): garbage collect with termination markers
1692 Some(None)
1693 }
1694 Generate::Continue => Some(None),
1695 }
1696 } else {
1697 Some(None)
1698 }
1699 })
1700 .splice_fn2_borrow_mut_ctx::<HashMap<K, Option<A>>, (K, V), _>(
1701 &OperatorContext::<L, B>::new(&this.location),
1702 )
1703 .into()
1704 });
1705
1706 let scan_node = HydroNode::Scan {
1707 init: scan_init,
1708 acc: scan_f,
1709 input: Box::new(this.ir_node.replace(HydroNode::Placeholder)),
1710 metadata: this.location.new_node_metadata(Stream::<
1711 Option<(K, U)>,
1712 L,
1713 B,
1714 TotalOrder,
1715 ExactlyOnce,
1716 >::collection_kind()),
1717 };
1718
1719 let flatten_f = q!(|d| d)
1720 .splice_fn1_ctx::<Option<(K, U)>, _>(&OperatorContext::<L, B>::new(&this.location))
1721 .into();
1722 let flatten_node = HydroNode::FlatMap {
1723 f: flatten_f,
1724 input: Box::new(scan_node),
1725 metadata: this.location.new_node_metadata(KeyedStream::<
1726 K,
1727 U,
1728 L,
1729 B,
1730 TotalOrder,
1731 ExactlyOnce,
1732 >::collection_kind()),
1733 };
1734
1735 KeyedStream::new(this.location.clone(), flatten_node)
1736 }
1737
1738 /// A variant of [`Stream::fold`], intended for keyed streams. The aggregation is executed
1739 /// in-order across the values in each group. But the aggregation function returns a boolean,
1740 /// which when true indicates that the aggregated result is complete and can be released to
1741 /// downstream computation. Unlike [`KeyedStream::fold`], this means that even if the input
1742 /// stream is [`super::boundedness::Unbounded`], the outputs of the fold can be processed like
1743 /// normal stream elements.
1744 ///
1745 /// # Example
1746 /// ```rust
1747 /// # #[cfg(feature = "deploy")] {
1748 /// # use hydro_lang::prelude::*;
1749 /// # use futures::StreamExt;
1750 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1751 /// process
1752 /// .source_iter(q!(vec![(0, 2), (0, 3), (1, 3), (1, 6)]))
1753 /// .into_keyed()
1754 /// .fold_early_stop(
1755 /// q!(|| 0),
1756 /// q!(|acc, x| {
1757 /// *acc += x;
1758 /// x % 2 == 0
1759 /// }),
1760 /// )
1761 /// # .entries()
1762 /// # }, |mut stream| async move {
1763 /// // Output: { 0: 2, 1: 9 }
1764 /// # let mut results = Vec::new();
1765 /// # for _ in 0..2 {
1766 /// # results.push(stream.next().await.unwrap());
1767 /// # }
1768 /// # results.sort();
1769 /// # assert_eq!(results, vec![(0, 2), (1, 9)]);
1770 /// # }));
1771 /// # }
1772 /// ```
1773 pub fn fold_early_stop<A, I, F>(
1774 self,
1775 init: impl IntoQuotedMut<'a, I, OperatorContext<L, B>> + Copy,
1776 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B>> + Copy,
1777 ) -> KeyedSingleton<K, A, L, B::WithBoundedValue>
1778 where
1779 O: IsOrdered,
1780 R: IsExactlyOnce,
1781 K: Clone + Eq + Hash,
1782 I: Fn() -> A + 'a,
1783 F: Fn(&mut A, V) -> bool + 'a,
1784 {
1785 let init: ManualExpr<I, _> =
1786 ManualExpr::new(move |ctx: &OperatorContext<L, B>| init.splice_fn0_ctx(ctx));
1787 let f: ManualExpr<F, _> =
1788 ManualExpr::new(move |ctx: &OperatorContext<L, B>| f.splice_fn2_borrow_mut_ctx(ctx));
1789 let out_without_bound_cast = self.generator(
1790 q!(move || Some(init())),
1791 q!(move |key_state, v| {
1792 if let Some(key_state_value) = key_state.as_mut() {
1793 if f(key_state_value, v) {
1794 Generate::Return(key_state.take().unwrap())
1795 } else {
1796 Generate::Continue
1797 }
1798 } else {
1799 unreachable!()
1800 }
1801 }),
1802 );
1803
1804 // SAFETY: The generator will only ever return at most one value per key, since once it
1805 // returns a value for a key it will never process any more values for that key.
1806 out_without_bound_cast.cast_at_most_one_entry_per_key()
1807 }
1808
1809 /// Gets the first element inside each group of values as a [`KeyedSingleton`] that preserves
1810 /// the original group keys. Requires the input stream to have [`TotalOrder`] guarantees,
1811 /// otherwise the first element would be non-deterministic.
1812 ///
1813 /// # Example
1814 /// ```rust
1815 /// # #[cfg(feature = "deploy")] {
1816 /// # use hydro_lang::prelude::*;
1817 /// # use futures::StreamExt;
1818 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1819 /// process
1820 /// .source_iter(q!(vec![(0, 2), (0, 3), (1, 3), (1, 6)]))
1821 /// .into_keyed()
1822 /// .first()
1823 /// # .entries()
1824 /// # }, |mut stream| async move {
1825 /// // Output: { 0: 2, 1: 3 }
1826 /// # let mut results = Vec::new();
1827 /// # for _ in 0..2 {
1828 /// # results.push(stream.next().await.unwrap());
1829 /// # }
1830 /// # results.sort();
1831 /// # assert_eq!(results, vec![(0, 2), (1, 3)]);
1832 /// # }));
1833 /// # }
1834 /// ```
1835 pub fn first(self) -> KeyedSingleton<K, V, L, B::WithBoundedValue>
1836 where
1837 O: IsOrdered,
1838 R: IsExactlyOnce,
1839 K: Clone + Eq + Hash,
1840 {
1841 self.fold_early_stop(
1842 q!(|| None),
1843 q!(|acc, v| {
1844 *acc = Some(v);
1845 true
1846 }),
1847 )
1848 .map(q!(|v| v.unwrap()))
1849 }
1850
1851 /// Returns a keyed stream containing at most the first `n` values per key,
1852 /// preserving the original order within each group. Similar to SQL `LIMIT`
1853 /// applied per group.
1854 ///
1855 /// This requires the stream to have a [`TotalOrder`] guarantee and [`ExactlyOnce`]
1856 /// retries, since the result depends on the order and cardinality of elements
1857 /// within each group.
1858 ///
1859 /// # Example
1860 /// ```rust
1861 /// # #[cfg(feature = "deploy")] {
1862 /// # use hydro_lang::prelude::*;
1863 /// # use futures::StreamExt;
1864 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1865 /// process
1866 /// .source_iter(q!(vec![(1, 10), (1, 20), (1, 30), (2, 40), (2, 50)]))
1867 /// .into_keyed()
1868 /// .limit(q!(2))
1869 /// # .entries()
1870 /// # }, |mut stream| async move {
1871 /// // { 1: [10, 20], 2: [40, 50] }
1872 /// # let mut results = Vec::new();
1873 /// # for _ in 0..4 {
1874 /// # results.push(stream.next().await.unwrap());
1875 /// # }
1876 /// # results.sort();
1877 /// # assert_eq!(results, vec![(1, 10), (1, 20), (2, 40), (2, 50)]);
1878 /// # }));
1879 /// # }
1880 /// ```
1881 pub fn limit(
1882 self,
1883 n: impl QuotedWithContext<'a, usize, OperatorContext<L, B>> + Copy + 'a,
1884 ) -> KeyedStream<K, V, L, B, TotalOrder, ExactlyOnce>
1885 where
1886 O: IsOrdered,
1887 R: IsExactlyOnce,
1888 K: Clone + Eq + Hash,
1889 {
1890 self.generator(
1891 q!(|| 0usize),
1892 q!(move |count, item| {
1893 if *count == n {
1894 Generate::Break
1895 } else {
1896 *count += 1;
1897 if *count == n {
1898 Generate::Return(item)
1899 } else {
1900 Generate::Yield(item)
1901 }
1902 }
1903 }),
1904 )
1905 }
1906
1907 /// Assigns a zero-based index to each value within each key group, emitting
1908 /// `(K, (index, V))` tuples with per-key sequential indices.
1909 ///
1910 /// The output keyed stream has [`TotalOrder`] and [`ExactlyOnce`] guarantees.
1911 /// This is a streaming operator that processes elements as they arrive.
1912 ///
1913 /// # Example
1914 /// ```rust
1915 /// # #[cfg(feature = "deploy")] {
1916 /// # use hydro_lang::prelude::*;
1917 /// # use futures::StreamExt;
1918 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1919 /// process
1920 /// .source_iter(q!(vec![(1, 10), (2, 20), (1, 30)]))
1921 /// .into_keyed()
1922 /// .enumerate()
1923 /// # .entries()
1924 /// # }, |mut stream| async move {
1925 /// // per-key indices: { 1: [(0, 10), (1, 30)], 2: [(0, 20)] }
1926 /// # let mut results = Vec::new();
1927 /// # for _ in 0..3 {
1928 /// # results.push(stream.next().await.unwrap());
1929 /// # }
1930 /// # let key1: Vec<_> = results.iter().filter(|(k, _)| *k == 1).map(|(_, v)| *v).collect();
1931 /// # let key2: Vec<_> = results.iter().filter(|(k, _)| *k == 2).map(|(_, v)| *v).collect();
1932 /// # assert_eq!(key1, vec![(0, 10), (1, 30)]);
1933 /// # assert_eq!(key2, vec![(0, 20)]);
1934 /// # }));
1935 /// # }
1936 /// ```
1937 pub fn enumerate(self) -> KeyedStream<K, (usize, V), L, B, TotalOrder, ExactlyOnce>
1938 where
1939 O: IsOrdered,
1940 R: IsExactlyOnce,
1941 K: Eq + Hash + Clone,
1942 {
1943 self.scan(
1944 q!(|| 0),
1945 q!(|acc, next| {
1946 let curr = *acc;
1947 *acc += 1;
1948 Some((curr, next))
1949 }),
1950 )
1951 }
1952
1953 /// Counts the number of elements in each group, producing a [`KeyedSingleton`] with the counts.
1954 ///
1955 /// # Example
1956 /// ```rust
1957 /// # #[cfg(feature = "deploy")] {
1958 /// # use hydro_lang::prelude::*;
1959 /// # use futures::StreamExt;
1960 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1961 /// let tick = process.tick();
1962 /// let numbers = process
1963 /// .source_iter(q!(vec![(1, 2), (2, 3), (1, 3), (2, 4), (1, 5)]))
1964 /// .into_keyed();
1965 /// let batch = numbers.batch(&tick, nondet!(/** test */));
1966 /// batch
1967 /// .value_counts()
1968 /// .entries()
1969 /// .all_ticks()
1970 /// # }, |mut stream| async move {
1971 /// // (1, 3), (2, 2)
1972 /// # let mut results = Vec::new();
1973 /// # for _ in 0..2 {
1974 /// # results.push(stream.next().await.unwrap());
1975 /// # }
1976 /// # results.sort();
1977 /// # assert_eq!(results, vec![(1, 3), (2, 2)]);
1978 /// # }));
1979 /// # }
1980 /// ```
1981 pub fn value_counts(
1982 self,
1983 ) -> KeyedSingleton<K, usize, L, <B as KeyedSingletonBound>::KeyedStreamToMonotone>
1984 where
1985 R: IsExactlyOnce,
1986 K: Eq + Hash,
1987 {
1988 self.make_exactly_once()
1989 .assume_ordering_trusted(
1990 nondet!(/** ordering within each group affects neither result nor intermediates */),
1991 )
1992 .fold(
1993 q!(|| 0),
1994 q!(
1995 |acc, _| *acc += 1,
1996 monotone = manual_proof!(/** += 1 is monotonic */)
1997 ),
1998 )
1999 }
2000
2001 /// Like [`Stream::fold`] but in the spirit of SQL `GROUP BY`, aggregates the values in each
2002 /// group via the `comb` closure.
2003 ///
2004 /// Depending on the input stream guarantees, the closure may need to be commutative
2005 /// (for unordered streams) or idempotent (for streams with non-deterministic duplicates).
2006 ///
2007 /// If the input and output value types are the same and do not require initialization then use
2008 /// [`KeyedStream::reduce`].
2009 ///
2010 /// # Example
2011 /// ```rust
2012 /// # #[cfg(feature = "deploy")] {
2013 /// # use hydro_lang::prelude::*;
2014 /// # use futures::StreamExt;
2015 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2016 /// let tick = process.tick();
2017 /// let numbers = process
2018 /// .source_iter(q!(vec![(1, false), (2, true), (1, false), (2, false)]))
2019 /// .into_keyed();
2020 /// let batch = numbers.batch(&tick, nondet!(/** test */));
2021 /// batch
2022 /// .fold(q!(|| false), q!(|acc, x| *acc |= x))
2023 /// .entries()
2024 /// .all_ticks()
2025 /// # }, |mut stream| async move {
2026 /// // (1, false), (2, true)
2027 /// # let mut results = Vec::new();
2028 /// # for _ in 0..2 {
2029 /// # results.push(stream.next().await.unwrap());
2030 /// # }
2031 /// # results.sort();
2032 /// # assert_eq!(results, vec![(1, false), (2, true)]);
2033 /// # }));
2034 /// # }
2035 /// ```
2036 pub fn fold<A, I: Fn() -> A + 'a, F: 'a + Fn(&mut A, V), C, Idemp, M, B2: KeyedSingletonBound>(
2037 self,
2038 init: impl IntoQuotedMut<'a, I, OperatorContext<L, B>>,
2039 comb: impl IntoQuotedMut<'a, F, OperatorContext<L, B>, AggFuncAlgebra<C, Idemp, M>>,
2040 ) -> KeyedSingleton<K, A, L, B2>
2041 where
2042 K: Eq + Hash,
2043 C: ValidCommutativityFor<O>,
2044 Idemp: ValidIdempotenceFor<R>,
2045 B: ApplyMonotoneKeyedStream<M, B2>,
2046 {
2047 let init = init
2048 .splice_fn0_ctx(&OperatorContext::<L, B>::new(&self.location))
2049 .into();
2050 let (comb, proof) =
2051 comb.splice_fn2_borrow_mut_ctx_props(&OperatorContext::<L, B>::new(&self.location));
2052 proof.register_proof(&comb);
2053
2054 let retried = self
2055 .assume_retries::<ExactlyOnce>(nondet!(/** the combinator function is idempotent */));
2056
2057 KeyedSingleton::new(
2058 retried.location.clone(),
2059 HydroNode::FoldKeyed {
2060 init,
2061 acc: comb.into(),
2062 input: Box::new(retried.ir_node.replace(HydroNode::Placeholder)),
2063 metadata: retried
2064 .location
2065 .new_node_metadata(KeyedSingleton::<K, A, L, B2>::collection_kind()),
2066 },
2067 )
2068 .assert_has_consistency_of(manual_proof!(/** algebraic properties */))
2069 }
2070
2071 /// Like [`Stream::reduce`] but in the spirit of SQL `GROUP BY`, aggregates the values in each
2072 /// group via the `comb` closure.
2073 ///
2074 /// Depending on the input stream guarantees, the closure may need to be commutative
2075 /// (for unordered streams) or idempotent (for streams with non-deterministic duplicates).
2076 ///
2077 /// If you need the accumulated value to have a different type than the input, use [`KeyedStream::fold`].
2078 ///
2079 /// # Example
2080 /// ```rust
2081 /// # #[cfg(feature = "deploy")] {
2082 /// # use hydro_lang::prelude::*;
2083 /// # use futures::StreamExt;
2084 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2085 /// let tick = process.tick();
2086 /// let numbers = process
2087 /// .source_iter(q!(vec![(1, false), (2, true), (1, false), (2, false)]))
2088 /// .into_keyed();
2089 /// let batch = numbers.batch(&tick, nondet!(/** test */));
2090 /// batch
2091 /// .reduce(q!(|acc, x| *acc |= x))
2092 /// .entries()
2093 /// .all_ticks()
2094 /// # }, |mut stream| async move {
2095 /// // (1, false), (2, true)
2096 /// # let mut results = Vec::new();
2097 /// # for _ in 0..2 {
2098 /// # results.push(stream.next().await.unwrap());
2099 /// # }
2100 /// # results.sort();
2101 /// # assert_eq!(results, vec![(1, false), (2, true)]);
2102 /// # }));
2103 /// # }
2104 /// ```
2105 pub fn reduce<F: Fn(&mut V, V) + 'a, C, Idemp>(
2106 self,
2107 comb: impl IntoQuotedMut<'a, F, OperatorContext<L, B>, AggFuncAlgebra<C, Idemp>>,
2108 ) -> KeyedSingleton<K, V, L, B>
2109 where
2110 K: Eq + Hash,
2111 C: ValidCommutativityFor<O>,
2112 Idemp: ValidIdempotenceFor<R>,
2113 {
2114 let (f, proof) =
2115 comb.splice_fn2_borrow_mut_ctx_props(&OperatorContext::<L, B>::new(&self.location));
2116 proof.register_proof(&f);
2117
2118 let ordered = self
2119 .assume_retries::<ExactlyOnce>(nondet!(/** the combinator function is idempotent */))
2120 .assume_ordering::<TotalOrder>(nondet!(/** the combinator function is commutative */));
2121
2122 KeyedSingleton::new(
2123 ordered.location.clone(),
2124 HydroNode::ReduceKeyed {
2125 f: f.into(),
2126 input: Box::new(ordered.ir_node.replace(HydroNode::Placeholder)),
2127 metadata: ordered
2128 .location
2129 .new_node_metadata(KeyedSingleton::<K, V, L, B>::collection_kind()),
2130 },
2131 )
2132 .assert_has_consistency_of(manual_proof!(/** algebraic properties */))
2133 }
2134
2135 /// A special case of [`KeyedStream::reduce`] where tuples with keys less than the watermark
2136 /// are automatically deleted.
2137 ///
2138 /// Depending on the input stream guarantees, the closure may need to be commutative
2139 /// (for unordered streams) or idempotent (for streams with non-deterministic duplicates).
2140 ///
2141 /// # Example
2142 /// ```rust
2143 /// # #[cfg(feature = "deploy")] {
2144 /// # use hydro_lang::prelude::*;
2145 /// # use futures::StreamExt;
2146 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2147 /// let tick = process.tick();
2148 /// let watermark = tick.singleton(q!(2));
2149 /// let numbers = process
2150 /// .source_iter(q!([(0, false), (1, false), (2, false), (2, true)]))
2151 /// .into_keyed();
2152 /// let batch = numbers.batch(&tick, nondet!(/** test */));
2153 /// batch
2154 /// .reduce_watermark(watermark, q!(|acc, x| *acc |= x))
2155 /// .entries()
2156 /// .all_ticks()
2157 /// # }, |mut stream| async move {
2158 /// // (2, true)
2159 /// # assert_eq!(stream.next().await.unwrap(), (2, true));
2160 /// # }));
2161 /// # }
2162 /// ```
2163 pub fn reduce_watermark<O2, F, C, Idemp>(
2164 self,
2165 other: impl Into<Optional<O2, Tick<L::Root>, Bounded>>,
2166 comb: impl IntoQuotedMut<'a, F, OperatorContext<L, B>, AggFuncAlgebra<C, Idemp>>,
2167 ) -> KeyedSingleton<K, V, L, B>
2168 where
2169 K: Eq + Hash,
2170 O2: Clone,
2171 F: Fn(&mut V, V) + 'a,
2172 C: ValidCommutativityFor<O>,
2173 Idemp: ValidIdempotenceFor<R>,
2174 {
2175 let other: Optional<O2, Tick<L::Root>, Bounded> = other.into();
2176 check_matching_location(&self.location.root(), other.location.outer());
2177 let (f, proof) =
2178 comb.splice_fn2_borrow_mut_ctx_props(&OperatorContext::<L, B>::new(&self.location));
2179 proof.register_proof(&f);
2180
2181 let ordered = self
2182 .assume_retries::<ExactlyOnce>(nondet!(/** the combinator function is idempotent */))
2183 .assume_ordering::<TotalOrder>(nondet!(/** the combinator function is commutative */));
2184
2185 KeyedSingleton::new(
2186 ordered.location.clone(),
2187 HydroNode::ReduceKeyedWatermark {
2188 f: f.into(),
2189 input: Box::new(ordered.ir_node.replace(HydroNode::Placeholder)),
2190 watermark: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
2191 metadata: ordered
2192 .location
2193 .new_node_metadata(KeyedSingleton::<K, V, L, B>::collection_kind()),
2194 },
2195 )
2196 .assert_has_consistency_of(manual_proof!(/** algebraic properties */))
2197 }
2198
2199 /// Given a bounded stream of keys `K`, returns a new keyed stream containing only the groups
2200 /// whose keys are not in the bounded stream.
2201 ///
2202 /// # Example
2203 /// ```rust
2204 /// # #[cfg(feature = "deploy")] {
2205 /// # use hydro_lang::prelude::*;
2206 /// # use futures::StreamExt;
2207 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2208 /// let tick = process.tick();
2209 /// let keyed_stream = process
2210 /// .source_iter(q!(vec![ (1, 'a'), (2, 'b'), (3, 'c'), (4, 'd') ]))
2211 /// .batch(&tick, nondet!(/** test */))
2212 /// .into_keyed();
2213 /// let keys_to_remove = process
2214 /// .source_iter(q!(vec![1, 2]))
2215 /// .batch(&tick, nondet!(/** test */));
2216 /// keyed_stream.filter_key_not_in(keys_to_remove).all_ticks()
2217 /// # .entries()
2218 /// # }, |mut stream| async move {
2219 /// // { 3: ['c'], 4: ['d'] }
2220 /// # let mut results = Vec::new();
2221 /// # for _ in 0..2 {
2222 /// # results.push(stream.next().await.unwrap());
2223 /// # }
2224 /// # results.sort();
2225 /// # assert_eq!(results, vec![(3, 'c'), (4, 'd')]);
2226 /// # }));
2227 /// # }
2228 /// ```
2229 pub fn filter_key_not_in<O2: Ordering, R2: Retries>(
2230 self,
2231 other: Stream<K, L, Bounded, O2, R2>,
2232 ) -> Self
2233 where
2234 K: Eq + Hash,
2235 {
2236 check_matching_location(&self.location, &other.location);
2237
2238 KeyedStream::new(
2239 self.location.clone(),
2240 HydroNode::AntiJoin {
2241 pos: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2242 neg: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
2243 metadata: self.location.new_node_metadata(Self::collection_kind()),
2244 },
2245 )
2246 }
2247
2248 /// Emit a keyed stream containing keys shared between two keyed streams,
2249 /// where each value in the output keyed stream is a tuple of
2250 /// (self's value, other's value).
2251 /// If there are multiple values for the same key, this performs a cross product
2252 /// for each matching key.
2253 ///
2254 /// # Example
2255 /// ```rust
2256 /// # #[cfg(feature = "deploy")] {
2257 /// # use hydro_lang::prelude::*;
2258 /// # use futures::StreamExt;
2259 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2260 /// let tick = process.tick();
2261 /// let keyed_data = process
2262 /// .source_iter(q!(vec![(1, 10), (1, 11), (2, 20)]))
2263 /// .into_keyed()
2264 /// .batch(&tick, nondet!(/** test */));
2265 /// let other_data = process
2266 /// .source_iter(q!(vec![(1, 100), (2, 200), (2, 201)]))
2267 /// .into_keyed()
2268 /// .batch(&tick, nondet!(/** test */));
2269 /// keyed_data.join_keyed_stream(other_data).entries().all_ticks()
2270 /// # }, |mut stream| async move {
2271 /// // { 1: [(10, 100), (11, 100)], 2: [(20, 200), (20, 201)] } in any order
2272 /// # let mut results = vec![];
2273 /// # for _ in 0..4 {
2274 /// # results.push(stream.next().await.unwrap());
2275 /// # }
2276 /// # results.sort();
2277 /// # assert_eq!(results, vec![(1, (10, 100)), (1, (11, 100)), (2, (20, 200)), (2, (20, 201))]);
2278 /// # }));
2279 /// # }
2280 /// ```
2281 pub fn join_keyed_stream<V2, B2: Boundedness, O2: Ordering, R2: Retries>(
2282 self,
2283 other: KeyedStream<K, V2, L, B2, O2, R2>,
2284 ) -> KeyedStream<
2285 K,
2286 (V, V2),
2287 L,
2288 B,
2289 B2::PreserveOrderIfBounded<NoOrder>,
2290 <R as MinRetries<R2>>::Min,
2291 >
2292 where
2293 K: Eq + Hash + Clone,
2294 R: MinRetries<R2>,
2295 V: Clone,
2296 V2: Clone,
2297 {
2298 self.entries().join(other.entries()).into_keyed()
2299 }
2300
2301 /// Deduplicates values within each key group, emitting each unique value per key
2302 /// exactly once.
2303 ///
2304 /// # Example
2305 /// ```rust
2306 /// # #[cfg(feature = "deploy")] {
2307 /// # use hydro_lang::prelude::*;
2308 /// # use futures::StreamExt;
2309 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2310 /// process
2311 /// .source_iter(q!(vec![(1, 10), (2, 20), (1, 10), (2, 30), (1, 20)]))
2312 /// .into_keyed()
2313 /// .unique()
2314 /// # .entries()
2315 /// # }, |mut stream| async move {
2316 /// // unique values per key: { 1: [10, 20], 2: [20, 30] }
2317 /// # let mut results = Vec::new();
2318 /// # for _ in 0..4 {
2319 /// # results.push(stream.next().await.unwrap());
2320 /// # }
2321 /// # let mut key1: Vec<_> = results.iter().filter(|(k, _)| *k == 1).map(|(_, v)| *v).collect();
2322 /// # let mut key2: Vec<_> = results.iter().filter(|(k, _)| *k == 2).map(|(_, v)| *v).collect();
2323 /// # key1.sort();
2324 /// # key2.sort();
2325 /// # assert_eq!(key1, vec![10, 20]);
2326 /// # assert_eq!(key2, vec![20, 30]);
2327 /// # }));
2328 /// # }
2329 /// ```
2330 pub fn unique(self) -> KeyedStream<K, V, L, B, NoOrder, ExactlyOnce>
2331 where
2332 K: Eq + Hash + Clone,
2333 V: Eq + Hash + Clone,
2334 {
2335 self.entries().unique().into_keyed()
2336 }
2337
2338 /// Sorts the values within each key group in ascending order.
2339 ///
2340 /// The output keyed stream has a [`TotalOrder`] guarantee on the values within
2341 /// each group. This operator will block until all elements in the input stream
2342 /// are available, so it requires the input stream to be [`Bounded`].
2343 ///
2344 /// # Example
2345 /// ```rust
2346 /// # #[cfg(feature = "deploy")] {
2347 /// # use hydro_lang::prelude::*;
2348 /// # use futures::StreamExt;
2349 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2350 /// let tick = process.tick();
2351 /// let numbers = process
2352 /// .source_iter(q!(vec![(1, 3), (2, 1), (1, 1), (2, 2)]))
2353 /// .into_keyed();
2354 /// let batch = numbers.batch(&tick, nondet!(/** test */));
2355 /// batch.sort().all_ticks()
2356 /// # .entries()
2357 /// # }, |mut stream| async move {
2358 /// // values sorted within each key: { 1: [1, 3], 2: [1, 2] }
2359 /// # let mut results = Vec::new();
2360 /// # for _ in 0..4 {
2361 /// # results.push(stream.next().await.unwrap());
2362 /// # }
2363 /// # let key1_vals: Vec<_> = results.iter().filter(|(k, _)| *k == 1).map(|(_, v)| *v).collect();
2364 /// # let key2_vals: Vec<_> = results.iter().filter(|(k, _)| *k == 2).map(|(_, v)| *v).collect();
2365 /// # assert_eq!(key1_vals, vec![1, 3]);
2366 /// # assert_eq!(key2_vals, vec![1, 2]);
2367 /// # }));
2368 /// # }
2369 /// ```
2370 pub fn sort(self) -> KeyedStream<K, V, L, Bounded, TotalOrder, R>
2371 where
2372 B: IsBounded,
2373 K: Ord,
2374 V: Ord,
2375 {
2376 self.entries().sort().into_keyed()
2377 }
2378
2379 /// Produces a new keyed stream that combines the groups of the inputs by first emitting the
2380 /// elements of the `self` stream, and then emits the elements of the `other` stream (if a key
2381 /// is only present in one of the inputs, its values are passed through as-is). The output has
2382 /// a [`TotalOrder`] guarantee if and only if both inputs have a [`TotalOrder`] guarantee.
2383 ///
2384 /// Currently, both input streams must be [`Bounded`]. This operator will block
2385 /// on the first stream until all its elements are available. In a future version,
2386 /// we will relax the requirement on the `other` stream.
2387 ///
2388 /// # Example
2389 /// ```rust
2390 /// # #[cfg(feature = "deploy")] {
2391 /// # use hydro_lang::prelude::*;
2392 /// # use futures::StreamExt;
2393 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2394 /// let tick = process.tick();
2395 /// let numbers = process.source_iter(q!(vec![(0, 1), (1, 3)])).into_keyed();
2396 /// let batch = numbers.batch(&tick, nondet!(/** test */));
2397 /// batch.clone().map(q!(|x| x + 1)).chain(batch).all_ticks()
2398 /// # .entries()
2399 /// # }, |mut stream| async move {
2400 /// // { 0: [2, 1], 1: [4, 3] }
2401 /// # let mut results = Vec::new();
2402 /// # for _ in 0..4 {
2403 /// # results.push(stream.next().await.unwrap());
2404 /// # }
2405 /// # results.sort();
2406 /// # assert_eq!(results, vec![(0, 1), (0, 2), (1, 3), (1, 4)]);
2407 /// # }));
2408 /// # }
2409 /// ```
2410 pub fn chain<O2: Ordering, R2: Retries>(
2411 self,
2412 other: KeyedStream<K, V, L, Bounded, O2, R2>,
2413 ) -> KeyedStream<K, V, L, Bounded, <O as MinOrder<O2>>::Min, <R as MinRetries<R2>>::Min>
2414 where
2415 B: IsBounded,
2416 O: MinOrder<O2>,
2417 R: MinRetries<R2>,
2418 {
2419 let this = self.make_bounded();
2420 check_matching_location(&this.location, &other.location);
2421
2422 KeyedStream::new(
2423 this.location.clone(),
2424 HydroNode::Chain {
2425 first: Box::new(this.ir_node.replace(HydroNode::Placeholder)),
2426 second: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
2427 metadata: this.location.new_node_metadata(KeyedStream::<
2428 K,
2429 V,
2430 L,
2431 Bounded,
2432 <O as MinOrder<O2>>::Min,
2433 <R as MinRetries<R2>>::Min,
2434 >::collection_kind()),
2435 },
2436 )
2437 }
2438
2439 /// Emit a keyed stream containing keys shared between the keyed stream and the
2440 /// keyed singleton, where each value in the output keyed stream is a tuple of
2441 /// (the keyed stream's value, the keyed singleton's value).
2442 ///
2443 /// # Example
2444 /// ```rust
2445 /// # #[cfg(feature = "deploy")] {
2446 /// # use hydro_lang::prelude::*;
2447 /// # use futures::StreamExt;
2448 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2449 /// let tick = process.tick();
2450 /// let keyed_data = process
2451 /// .source_iter(q!(vec![(1, 10), (1, 11), (2, 20)]))
2452 /// .into_keyed()
2453 /// .batch(&tick, nondet!(/** test */));
2454 /// let singleton_data = process
2455 /// .source_iter(q!(vec![(1, 100), (2, 200)]))
2456 /// .into_keyed()
2457 /// .batch(&tick, nondet!(/** test */))
2458 /// .first();
2459 /// keyed_data.join_keyed_singleton(singleton_data).entries().all_ticks()
2460 /// # }, |mut stream| async move {
2461 /// // { 1: [(10, 100), (11, 100)], 2: [(20, 200)] } in any order
2462 /// # let mut results = vec![];
2463 /// # for _ in 0..3 {
2464 /// # results.push(stream.next().await.unwrap());
2465 /// # }
2466 /// # results.sort();
2467 /// # assert_eq!(results, vec![(1, (10, 100)), (1, (11, 100)), (2, (20, 200))]);
2468 /// # }));
2469 /// # }
2470 /// ```
2471 pub fn join_keyed_singleton<V2: Clone, B2: IsBounded>(
2472 self,
2473 other: KeyedSingleton<K, V2, L, B2>,
2474 ) -> KeyedStream<K, (V, V2), L, B, O, R>
2475 where
2476 K: Eq + Hash + Clone,
2477 V: Clone,
2478 {
2479 let ir_node = if B2::BOUNDED {
2480 HydroNode::JoinHalf {
2481 left: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2482 right: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
2483 metadata: self
2484 .location
2485 .new_node_metadata(KeyedStream::<K, (V, V2), L, B, O, R>::collection_kind()),
2486 }
2487 } else {
2488 HydroNode::Join {
2489 left: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2490 right: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
2491 metadata: self
2492 .location
2493 .new_node_metadata(KeyedStream::<K, (V, V2), L, B, O, R>::collection_kind()),
2494 }
2495 };
2496
2497 KeyedStream::new(self.location.clone(), ir_node)
2498 }
2499
2500 /// Gets the values associated with a specific key from the keyed stream.
2501 /// Returns an empty stream if the key is `None` or there are no associated values.
2502 ///
2503 /// # Example
2504 /// ```rust
2505 /// # #[cfg(feature = "deploy")] {
2506 /// # use hydro_lang::prelude::*;
2507 /// # use futures::StreamExt;
2508 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2509 /// let tick = process.tick();
2510 /// let keyed_data = process
2511 /// .source_iter(q!(vec![(1, 10), (1, 11), (2, 20)]))
2512 /// .into_keyed()
2513 /// .batch(&tick, nondet!(/** test */));
2514 /// let key = tick.singleton(q!(1));
2515 /// keyed_data.get(key).all_ticks()
2516 /// # }, |mut stream| async move {
2517 /// // 10, 11
2518 /// # let mut results = vec![];
2519 /// # for _ in 0..2 {
2520 /// # results.push(stream.next().await.unwrap());
2521 /// # }
2522 /// # results.sort();
2523 /// # assert_eq!(results, vec![10, 11]);
2524 /// # }));
2525 /// # }
2526 /// ```
2527 pub fn get(self, key: impl Into<Optional<K, L, Bounded>>) -> Stream<V, L, B, O, R>
2528 where
2529 K: Eq + Hash + Clone,
2530 V: Clone,
2531 {
2532 let joined =
2533 self.join_keyed_singleton(key.into().map(q!(|k| (k, ()))).into_keyed_singleton());
2534
2535 if O::ORDERING_KIND == StreamOrder::TotalOrder {
2536 joined
2537 .use_ordering_type::<TotalOrder>()
2538 .cast_at_most_one_key()
2539 .map(q!(|(_, (v, _))| v))
2540 .weaken_ordering()
2541 } else {
2542 joined.values().map(q!(|(v, _)| v)).use_ordering_type()
2543 }
2544 }
2545
2546 /// For each value in `self`, find the matching key in `lookup`.
2547 /// The output is a keyed stream with the key from `self`, and a value
2548 /// that is a tuple of (`self`'s value, Option<`lookup`'s value>).
2549 /// If the key is not present in `lookup`, the option will be [`None`].
2550 ///
2551 /// # Example
2552 /// ```rust
2553 /// # #[cfg(feature = "deploy")] {
2554 /// # use hydro_lang::prelude::*;
2555 /// # use futures::StreamExt;
2556 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2557 /// # let tick = process.tick();
2558 /// let requests = // { 1: [10, 11], 2: 20 }
2559 /// # process
2560 /// # .source_iter(q!(vec![(1, 10), (1, 11), (2, 20)]))
2561 /// # .into_keyed()
2562 /// # .batch(&tick, nondet!(/** test */));
2563 /// let other_data = // { 10: 100, 11: 110 }
2564 /// # process
2565 /// # .source_iter(q!(vec![(10, 100), (11, 110)]))
2566 /// # .into_keyed()
2567 /// # .batch(&tick, nondet!(/** test */))
2568 /// # .first();
2569 /// requests.lookup_keyed_singleton(other_data)
2570 /// # .entries().all_ticks()
2571 /// # }, |mut stream| async move {
2572 /// // { 1: [(10, Some(100)), (11, Some(110))], 2: (20, None) }
2573 /// # let mut results = vec![];
2574 /// # for _ in 0..3 {
2575 /// # results.push(stream.next().await.unwrap());
2576 /// # }
2577 /// # results.sort();
2578 /// # assert_eq!(results, vec![(1, (10, Some(100))), (1, (11, Some(110))), (2, (20, None))]);
2579 /// # }));
2580 /// # }
2581 /// ```
2582 pub fn lookup_keyed_singleton<V2>(
2583 self,
2584 lookup: KeyedSingleton<V, V2, L, Bounded>,
2585 ) -> KeyedStream<K, (V, Option<V2>), L, Bounded, NoOrder, R>
2586 where
2587 B: IsBounded,
2588 K: Eq + Hash + Clone,
2589 V: Eq + Hash + Clone,
2590 V2: Clone,
2591 {
2592 self.lookup_keyed_stream(lookup.into_keyed_stream().weaken_retries::<R>())
2593 }
2594
2595 /// For each value in `self`, find the matching key in `lookup`.
2596 /// The output is a keyed stream with the key from `self`, and a value
2597 /// that is a tuple of (`self`'s value, Option<`lookup`'s value>).
2598 /// If the key is not present in `lookup`, the option will be [`None`].
2599 ///
2600 /// # Example
2601 /// ```rust
2602 /// # #[cfg(feature = "deploy")] {
2603 /// # use hydro_lang::prelude::*;
2604 /// # use futures::StreamExt;
2605 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2606 /// # let tick = process.tick();
2607 /// let requests = // { 1: [10, 11], 2: 20 }
2608 /// # process
2609 /// # .source_iter(q!(vec![(1, 10), (1, 11), (2, 20)]))
2610 /// # .into_keyed()
2611 /// # .batch(&tick, nondet!(/** test */));
2612 /// let other_data = // { 10: [100, 101], 11: 110 }
2613 /// # process
2614 /// # .source_iter(q!(vec![(10, 100), (10, 101), (11, 110)]))
2615 /// # .into_keyed()
2616 /// # .batch(&tick, nondet!(/** test */));
2617 /// requests.lookup_keyed_stream(other_data)
2618 /// # .entries().all_ticks()
2619 /// # }, |mut stream| async move {
2620 /// // { 1: [(10, Some(100)), (10, Some(101)), (11, Some(110))], 2: (20, None) }
2621 /// # let mut results = vec![];
2622 /// # for _ in 0..4 {
2623 /// # results.push(stream.next().await.unwrap());
2624 /// # }
2625 /// # results.sort();
2626 /// # assert_eq!(results, vec![(1, (10, Some(100))), (1, (10, Some(101))), (1, (11, Some(110))), (2, (20, None))]);
2627 /// # }));
2628 /// # }
2629 /// ```
2630 pub fn lookup_keyed_stream<V2, O2: Ordering, R2: Retries>(
2631 self,
2632 lookup: KeyedStream<V, V2, L, Bounded, O2, R2>,
2633 ) -> KeyedStream<K, (V, Option<V2>), L, Bounded, NoOrder, <R as MinRetries<R2>>::Min>
2634 where
2635 B: IsBounded,
2636 K: Eq + Hash + Clone,
2637 V: Eq + Hash + Clone,
2638 V2: Clone,
2639 R: MinRetries<R2>,
2640 {
2641 let inverted = self
2642 .make_bounded()
2643 .entries()
2644 .map(q!(|(key, lookup_value)| (lookup_value, key)))
2645 .into_keyed();
2646 let found = inverted
2647 .clone()
2648 .join_keyed_stream(lookup.clone())
2649 .entries()
2650 .map(q!(|(lookup_value, (key, value))| (
2651 key,
2652 (lookup_value, Some(value))
2653 )))
2654 .into_keyed();
2655 let not_found = inverted
2656 .filter_key_not_in(lookup.keys())
2657 .entries()
2658 .map(q!(|(lookup_value, key)| (key, (lookup_value, None))))
2659 .into_keyed();
2660
2661 found.chain(not_found.weaken_retries::<<R as MinRetries<R2>>::Min>())
2662 }
2663
2664 /// Shifts this keyed stream into an atomic context, which guarantees that any downstream logic
2665 /// will all be executed synchronously before any outputs are yielded (in [`KeyedStream::end_atomic`]).
2666 ///
2667 /// This is useful to enforce local consistency constraints, such as ensuring that a write is
2668 /// processed before an acknowledgement is emitted.
2669 pub fn atomic(self) -> KeyedStream<K, V, Atomic<L>, B, O, R> {
2670 let id = self.location.flow_state().borrow_mut().next_clock_id();
2671 let out_location = Atomic {
2672 tick: Tick {
2673 id,
2674 l: self.location.clone(),
2675 },
2676 };
2677 KeyedStream::new(
2678 out_location.clone(),
2679 HydroNode::BeginAtomic {
2680 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2681 metadata: out_location
2682 .new_node_metadata(KeyedStream::<K, V, Atomic<L>, B, O, R>::collection_kind()),
2683 },
2684 )
2685 }
2686
2687 /// Given a tick, returns a keyed stream corresponding to a batch of elements segmented by
2688 /// that tick. These batches are guaranteed to be contiguous across ticks and preserve
2689 /// the order of the input.
2690 ///
2691 /// # Non-Determinism
2692 /// The batch boundaries are non-deterministic and may change across executions.
2693 pub fn batch<L2: Location<'a, DropConsistency = L::DropConsistency>>(
2694 self,
2695 tick: &Tick<L2>,
2696 nondet: NonDet,
2697 ) -> KeyedStream<K, V, Tick<L::DropConsistency>, Bounded, O, R> {
2698 let _ = nondet;
2699 assert_eq!(Location::id(tick.outer()), Location::id(&self.location));
2700 KeyedStream::new(
2701 tick.drop_consistency(),
2702 HydroNode::Batch {
2703 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2704 metadata: tick.new_node_metadata(
2705 KeyedStream::<K, V, Tick<L>, Bounded, O, R>::collection_kind(),
2706 ),
2707 },
2708 )
2709 }
2710}
2711
2712impl<'a, K1, K2, V, L: Location<'a>, B: Boundedness, O: Ordering, R: Retries>
2713 KeyedStream<(K1, K2), V, L, B, O, R>
2714{
2715 /// Produces a new keyed stream by dropping the first element of the compound key.
2716 ///
2717 /// Because multiple keys may share the same suffix, this operation results in re-grouping
2718 /// of the values under the new keys. The values across groups with the same new key
2719 /// will be interleaved, so the resulting stream has [`NoOrder`] within each group.
2720 ///
2721 /// # Example
2722 /// ```rust
2723 /// # #[cfg(feature = "deploy")] {
2724 /// # use hydro_lang::prelude::*;
2725 /// # use futures::StreamExt;
2726 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2727 /// process
2728 /// .source_iter(q!(vec![((1, 10), 2), ((1, 10), 3), ((2, 20), 4)]))
2729 /// .into_keyed()
2730 /// .drop_key_prefix()
2731 /// # .entries()
2732 /// # }, |mut stream| async move {
2733 /// // { 10: [2, 3], 20: [4] }
2734 /// # let mut results = Vec::new();
2735 /// # for _ in 0..3 {
2736 /// # results.push(stream.next().await.unwrap());
2737 /// # }
2738 /// # results.sort();
2739 /// # assert_eq!(results, vec![(10, 2), (10, 3), (20, 4)]);
2740 /// # }));
2741 /// # }
2742 /// ```
2743 pub fn drop_key_prefix(self) -> KeyedStream<K2, V, L, B, NoOrder, R> {
2744 self.entries()
2745 .map(q!(|((_k1, k2), v)| (k2, v)))
2746 .into_keyed()
2747 }
2748}
2749
2750impl<'a, K, V, L: Location<'a>, O: Ordering, R: Retries> KeyedStream<K, V, L, Unbounded, O, R> {
2751 /// Produces a new keyed stream that "merges" the inputs by interleaving the elements
2752 /// of any overlapping groups. The result has [`NoOrder`] on each group because the
2753 /// order of interleaving is not guaranteed. If the keys across both inputs do not overlap,
2754 /// the ordering will be deterministic and you can safely use [`Self::assume_ordering`].
2755 ///
2756 /// Currently, both input streams must be [`Unbounded`].
2757 ///
2758 /// # Example
2759 /// ```rust
2760 /// # #[cfg(feature = "deploy")] {
2761 /// # use hydro_lang::prelude::*;
2762 /// # use futures::StreamExt;
2763 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2764 /// let numbers1: KeyedStream<i32, i32, _> = // { 1: [2], 3: [4] }
2765 /// # process.source_iter(q!(vec![(1, 2), (3, 4)])).into_keyed().into();
2766 /// let numbers2: KeyedStream<i32, i32, _> = // { 1: [3], 3: [5] }
2767 /// # process.source_iter(q!(vec![(1, 3), (3, 5)])).into_keyed().into();
2768 /// numbers1.merge_unordered(numbers2)
2769 /// # .entries()
2770 /// # }, |mut stream| async move {
2771 /// // { 1: [2, 3], 3: [4, 5] } with each group in unknown order
2772 /// # let mut results = Vec::new();
2773 /// # for _ in 0..4 {
2774 /// # results.push(stream.next().await.unwrap());
2775 /// # }
2776 /// # results.sort();
2777 /// # assert_eq!(results, vec![(1, 2), (1, 3), (3, 4), (3, 5)]);
2778 /// # }));
2779 /// # }
2780 /// ```
2781 pub fn merge_unordered<O2: Ordering, R2: Retries>(
2782 self,
2783 other: KeyedStream<K, V, L, Unbounded, O2, R2>,
2784 ) -> KeyedStream<K, V, L, Unbounded, NoOrder, <R as MinRetries<R2>>::Min>
2785 where
2786 R: MinRetries<R2>,
2787 {
2788 KeyedStream::new(
2789 self.location.clone(),
2790 HydroNode::Chain {
2791 first: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2792 second: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
2793 metadata: self.location.new_node_metadata(KeyedStream::<
2794 K,
2795 V,
2796 L,
2797 Unbounded,
2798 NoOrder,
2799 <R as MinRetries<R2>>::Min,
2800 >::collection_kind()),
2801 },
2802 )
2803 }
2804
2805 /// Deprecated: use [`KeyedStream::merge_unordered`] instead.
2806 #[deprecated(note = "use `merge_unordered` instead")]
2807 pub fn interleave<O2: Ordering, R2: Retries>(
2808 self,
2809 other: KeyedStream<K, V, L, Unbounded, O2, R2>,
2810 ) -> KeyedStream<K, V, L, Unbounded, NoOrder, <R as MinRetries<R2>>::Min>
2811 where
2812 R: MinRetries<R2>,
2813 {
2814 self.merge_unordered(other)
2815 }
2816}
2817
2818impl<'a, K, V, L: Location<'a>, B: Boundedness, R: Retries> KeyedStream<K, V, L, B, TotalOrder, R> {
2819 /// Produces a new keyed stream that combines the elements of the two input keyed streams,
2820 /// preserving the relative order of elements within each group of each input.
2821 ///
2822 /// Because each group in both inputs is [`TotalOrder`], the output preserves the relative
2823 /// order of elements within each group of each input, and the result is [`TotalOrder`].
2824 ///
2825 /// # Non-Determinism
2826 /// For groups whose key appears in both inputs, the order in which the elements of the two
2827 /// inputs are interleaved *within that group* is non-deterministic, so the order of elements
2828 /// will vary across runs. If the keys across both inputs do not overlap, the ordering is
2829 /// deterministic. If the output order within each group is irrelevant, use
2830 /// [`KeyedStream::merge_unordered`] instead, which is deterministic but emits an unordered
2831 /// keyed stream.
2832 ///
2833 /// # Example
2834 /// ```rust
2835 /// # #[cfg(feature = "deploy")] {
2836 /// # use hydro_lang::prelude::*;
2837 /// # use futures::StreamExt;
2838 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2839 /// let numbers1: KeyedStream<i32, i32, _> = // { 1: [2], 3: [4] }
2840 /// # process.source_iter(q!(vec![(1, 2), (3, 4)])).into_keyed().into();
2841 /// let numbers2: KeyedStream<i32, i32, _> = // { 1: [3], 3: [5] }
2842 /// # process.source_iter(q!(vec![(1, 3), (3, 5)])).into_keyed().into();
2843 /// numbers1.merge_ordered(numbers2, nondet!(/** example */))
2844 /// # .entries()
2845 /// # }, |mut stream| async move {
2846 /// // { 1: [2, 3], 3: [4, 5] } with each group interleaved in some order
2847 /// # let mut results = Vec::new();
2848 /// # for _ in 0..4 {
2849 /// # results.push(stream.next().await.unwrap());
2850 /// # }
2851 /// # results.sort();
2852 /// # assert_eq!(results, vec![(1, 2), (1, 3), (3, 4), (3, 5)]);
2853 /// # }));
2854 /// # }
2855 /// ```
2856 pub fn merge_ordered<R2: Retries>(
2857 self,
2858 other: KeyedStream<K, V, L, B, TotalOrder, R2>,
2859 _nondet: NonDet,
2860 ) -> KeyedStream<K, V, L::DropConsistency, B, TotalOrder, <R as MinRetries<R2>>::Min>
2861 where
2862 R: MinRetries<R2>,
2863 {
2864 let target_location = self.location.drop_consistency();
2865 KeyedStream::new(
2866 target_location.clone(),
2867 HydroNode::MergeOrdered {
2868 first: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2869 second: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
2870 metadata: target_location.new_node_metadata(KeyedStream::<
2871 K,
2872 V,
2873 L::DropConsistency,
2874 B,
2875 TotalOrder,
2876 <R as MinRetries<R2>>::Min,
2877 >::collection_kind()),
2878 },
2879 )
2880 }
2881}
2882
2883impl<'a, K, V, L, B: Boundedness, O: Ordering, R: Retries> KeyedStream<K, V, Atomic<L>, B, O, R>
2884where
2885 L: Location<'a>,
2886{
2887 /// Returns a keyed stream corresponding to the latest batch of elements being atomically
2888 /// processed. These batches are guaranteed to be contiguous across ticks and preserve
2889 /// the order of the input. The output keyed stream will execute in the [`Tick`] that was
2890 /// used to create the atomic section.
2891 ///
2892 /// # Non-Determinism
2893 /// The batch boundaries are non-deterministic and may change across executions.
2894 pub fn batch_atomic<L2: Location<'a, DropConsistency = L::DropConsistency>>(
2895 self,
2896 tick: &Tick<L2>,
2897 nondet: NonDet,
2898 ) -> KeyedStream<K, V, Tick<L::DropConsistency>, Bounded, O, R> {
2899 let _ = nondet;
2900 KeyedStream::new(
2901 tick.drop_consistency(),
2902 HydroNode::Batch {
2903 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2904 metadata: tick.new_node_metadata(
2905 KeyedStream::<K, V, Tick<L>, Bounded, O, R>::collection_kind(),
2906 ),
2907 },
2908 )
2909 }
2910
2911 /// Yields the elements of this keyed stream back into a top-level, asynchronous execution context.
2912 /// See [`KeyedStream::atomic`] for more details.
2913 pub fn end_atomic(self) -> KeyedStream<K, V, L, B, O, R> {
2914 KeyedStream::new(
2915 self.location.tick.l.clone(),
2916 HydroNode::EndAtomic {
2917 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2918 metadata: self
2919 .location
2920 .tick
2921 .l
2922 .new_node_metadata(KeyedStream::<K, V, L, B, O, R>::collection_kind()),
2923 },
2924 )
2925 }
2926}
2927
2928impl<'a, K, V, L, O: Ordering, R: Retries> KeyedStream<K, V, Tick<L>, Bounded, O, R>
2929where
2930 L: Location<'a>,
2931{
2932 /// Asynchronously yields this batch of keyed elements outside the tick as an unbounded keyed stream,
2933 /// which will stream all the elements across _all_ tick iterations by concatenating the batches for
2934 /// each key.
2935 pub fn all_ticks(self) -> KeyedStream<K, V, L, Unbounded, O, R> {
2936 KeyedStream::new(
2937 self.location.outer().clone(),
2938 HydroNode::YieldConcat {
2939 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2940 metadata: self.location.outer().new_node_metadata(KeyedStream::<
2941 K,
2942 V,
2943 L,
2944 Unbounded,
2945 O,
2946 R,
2947 >::collection_kind(
2948 )),
2949 },
2950 )
2951 }
2952
2953 /// Synchronously yields this batch of keyed elements outside the tick as an unbounded keyed stream,
2954 /// which will stream all the elements across _all_ tick iterations by concatenating the batches for
2955 /// each key.
2956 ///
2957 /// Unlike [`KeyedStream::all_ticks`], this preserves synchronous execution, as the output stream
2958 /// is emitted in an [`Atomic`] context that will process elements synchronously with the input
2959 /// stream's [`Tick`] context.
2960 pub fn all_ticks_atomic(self) -> KeyedStream<K, V, Atomic<L>, Unbounded, O, R> {
2961 let out_location = Atomic {
2962 tick: self.location.clone(),
2963 };
2964
2965 KeyedStream::new(
2966 out_location.clone(),
2967 HydroNode::YieldConcat {
2968 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2969 metadata: out_location.new_node_metadata(KeyedStream::<
2970 K,
2971 V,
2972 Atomic<L>,
2973 Unbounded,
2974 O,
2975 R,
2976 >::collection_kind()),
2977 },
2978 )
2979 }
2980
2981 /// Transforms the keyed stream using the given closure in "stateful" mode, where stateful operators
2982 /// such as `fold` retrain their memory for each key across ticks rather than resetting across batches of each key.
2983 ///
2984 /// This API is particularly useful for stateful computation on batches of data, such as
2985 /// maintaining an accumulated state that is up to date with the current batch.
2986 ///
2987 /// # Example
2988 /// ```rust
2989 /// # #[cfg(feature = "deploy")] {
2990 /// # use hydro_lang::prelude::*;
2991 /// # use futures::StreamExt;
2992 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
2993 /// let tick = process.tick();
2994 /// # // ticks are lazy by default, forces the second tick to run
2995 /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
2996 /// # let batch_first_tick = process
2997 /// # .source_iter(q!(vec![(0, 1), (1, 2), (2, 3), (3, 4)]))
2998 /// # .into_keyed()
2999 /// # .batch(&tick, nondet!(/** test */));
3000 /// # let batch_second_tick = process
3001 /// # .source_iter(q!(vec![(0, 5), (1, 6), (2, 7)]))
3002 /// # .into_keyed()
3003 /// # .batch(&tick, nondet!(/** test */))
3004 /// # .defer_tick(); // appears on the second tick
3005 /// let input = batch_first_tick.chain(batch_second_tick).all_ticks();
3006 ///
3007 /// input.batch(&tick, nondet!(/** test */))
3008 /// .across_ticks(|s| s.reduce(q!(|sum, new| {
3009 /// *sum += new;
3010 /// }))).entries().all_ticks()
3011 /// # }, |mut stream| async move {
3012 /// // First tick: [(0, 1), (1, 2), (2, 3), (3, 4)]
3013 /// # let mut results = Vec::new();
3014 /// # for _ in 0..4 {
3015 /// # results.push(stream.next().await.unwrap());
3016 /// # }
3017 /// # results.sort();
3018 /// # assert_eq!(results, vec![(0, 1), (1, 2), (2, 3), (3, 4)]);
3019 /// // Second tick: [(0, 6), (1, 8), (2, 10), (3, 4)]
3020 /// # results.clear();
3021 /// # for _ in 0..4 {
3022 /// # results.push(stream.next().await.unwrap());
3023 /// # }
3024 /// # results.sort();
3025 /// # assert_eq!(results, vec![(0, 6), (1, 8), (2, 10), (3, 4)]);
3026 /// # }));
3027 /// # }
3028 /// ```
3029 pub fn across_ticks<Out: BatchAtomic<'a>>(
3030 self,
3031 thunk: impl FnOnce(KeyedStream<K, V, Atomic<L>, Unbounded, O, R>) -> Out,
3032 ) -> Out::Batched {
3033 thunk(self.all_ticks_atomic()).batched_atomic()
3034 }
3035
3036 /// Shifts the entries in `self` to the **next tick**, so that the returned keyed stream at
3037 /// tick `T` always has the entries of `self` at tick `T - 1`.
3038 ///
3039 /// At tick `0`, the output keyed stream is empty, since there is no previous tick.
3040 ///
3041 /// This operator enables stateful iterative processing with ticks, by sending data from one
3042 /// tick to the next. For example, you can use it to combine inputs across consecutive batches.
3043 ///
3044 /// # Example
3045 /// ```rust
3046 /// # #[cfg(feature = "deploy")] {
3047 /// # use hydro_lang::prelude::*;
3048 /// # use futures::StreamExt;
3049 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
3050 /// let tick = process.tick();
3051 /// # // ticks are lazy by default, forces the second tick to run
3052 /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
3053 /// # let batch_first_tick = process
3054 /// # .source_iter(q!(vec![(1, 2), (1, 3)]))
3055 /// # .batch(&tick, nondet!(/** test */))
3056 /// # .into_keyed();
3057 /// # let batch_second_tick = process
3058 /// # .source_iter(q!(vec![(1, 4), (2, 5)]))
3059 /// # .batch(&tick, nondet!(/** test */))
3060 /// # .defer_tick()
3061 /// # .into_keyed(); // appears on the second tick
3062 /// let changes_across_ticks = // { 1: [2, 3] } (first tick), { 1: [4], 2: [5] } (second tick)
3063 /// # batch_first_tick.chain(batch_second_tick);
3064 /// changes_across_ticks.clone().defer_tick().chain( // from the previous tick
3065 /// changes_across_ticks // from the current tick
3066 /// )
3067 /// # .entries().all_ticks()
3068 /// # }, |mut stream| async move {
3069 /// // First tick: { 1: [2, 3] }
3070 /// # let mut results = Vec::new();
3071 /// # for _ in 0..2 {
3072 /// # results.push(stream.next().await.unwrap());
3073 /// # }
3074 /// # results.sort();
3075 /// # assert_eq!(results, vec![(1, 2), (1, 3)]);
3076 /// // Second tick: { 1: [2, 3, 4], 2: [5] }
3077 /// # results.clear();
3078 /// # for _ in 0..4 {
3079 /// # results.push(stream.next().await.unwrap());
3080 /// # }
3081 /// # results.sort();
3082 /// # assert_eq!(results, vec![(1, 2), (1, 3), (1, 4), (2, 5)]);
3083 /// // Third tick: { 1: [4], 2: [5] }
3084 /// # results.clear();
3085 /// # for _ in 0..2 {
3086 /// # results.push(stream.next().await.unwrap());
3087 /// # }
3088 /// # results.sort();
3089 /// # assert_eq!(results, vec![(1, 4), (2, 5)]);
3090 /// # }));
3091 /// # }
3092 /// ```
3093 pub fn defer_tick(self) -> KeyedStream<K, V, Tick<L>, Bounded, O, R> {
3094 KeyedStream::new(
3095 self.location.clone(),
3096 HydroNode::DeferTick {
3097 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
3098 metadata: self.location.new_node_metadata(KeyedStream::<
3099 K,
3100 V,
3101 Tick<L>,
3102 Bounded,
3103 O,
3104 R,
3105 >::collection_kind()),
3106 },
3107 )
3108 }
3109}
3110
3111#[cfg(test)]
3112mod tests {
3113 #[cfg(feature = "deploy")]
3114 use futures::{SinkExt, StreamExt};
3115 #[cfg(feature = "deploy")]
3116 use hydro_deploy::Deployment;
3117 #[cfg(any(feature = "deploy", feature = "sim"))]
3118 use stageleft::q;
3119
3120 #[cfg(any(feature = "deploy", feature = "sim"))]
3121 use crate::compile::builder::FlowBuilder;
3122 #[cfg(feature = "deploy")]
3123 use crate::live_collections::stream::ExactlyOnce;
3124 #[cfg(feature = "sim")]
3125 use crate::live_collections::stream::{NoOrder, TotalOrder};
3126 #[cfg(any(feature = "deploy", feature = "sim"))]
3127 use crate::location::Location;
3128 #[cfg(feature = "sim")]
3129 use crate::networking::TCP;
3130 #[cfg(any(feature = "deploy", feature = "sim"))]
3131 use crate::nondet::nondet;
3132 #[cfg(feature = "deploy")]
3133 use crate::properties::manual_proof;
3134
3135 #[cfg(feature = "deploy")]
3136 #[tokio::test]
3137 async fn get_unbounded_keyed_stream_bounded_singleton() {
3138 let mut deployment = Deployment::new();
3139
3140 let mut flow = FlowBuilder::new();
3141 let node = flow.process::<()>();
3142 let external = flow.external::<()>();
3143
3144 let (input_send, input_stream) =
3145 node.source_external_bincode::<_, (i32, i32), _, ExactlyOnce>(&external);
3146
3147 let key = node.singleton(q!(1));
3148
3149 let out = input_stream
3150 .into_keyed()
3151 .get(key)
3152 .send_bincode_external(&external);
3153
3154 let nodes = flow
3155 .with_process(&node, deployment.Localhost())
3156 .with_external(&external, deployment.Localhost())
3157 .deploy(&mut deployment);
3158
3159 deployment.deploy().await.unwrap();
3160
3161 let mut input_send = nodes.connect(input_send).await;
3162 let mut out = nodes.connect(out).await;
3163
3164 deployment.start().await.unwrap();
3165
3166 // First batch
3167 input_send.send((1, 10)).await.unwrap();
3168 input_send.send((2, 20)).await.unwrap();
3169 assert_eq!(out.next().await.unwrap(), 10);
3170
3171 // Second batch
3172 input_send.send((1, 11)).await.unwrap();
3173 input_send.send((2, 21)).await.unwrap();
3174 assert_eq!(out.next().await.unwrap(), 11);
3175 }
3176
3177 #[cfg(feature = "deploy")]
3178 #[tokio::test]
3179 async fn reduce_watermark_filter() {
3180 let mut deployment = Deployment::new();
3181
3182 let mut flow = FlowBuilder::new();
3183 let node = flow.process::<()>();
3184 let external = flow.external::<()>();
3185
3186 let node_tick = node.tick();
3187 let watermark = node_tick.singleton(q!(2));
3188
3189 let sum = node
3190 .source_stream(q!(tokio_stream::iter([
3191 (0, 100),
3192 (1, 101),
3193 (2, 102),
3194 (2, 102)
3195 ])))
3196 .into_keyed()
3197 .reduce_watermark(
3198 watermark,
3199 q!(|acc, v| {
3200 *acc += v;
3201 }),
3202 )
3203 .snapshot(&node_tick, nondet!(/** test */))
3204 .entries()
3205 .all_ticks()
3206 .send_bincode_external(&external);
3207
3208 let nodes = flow
3209 .with_process(&node, deployment.Localhost())
3210 .with_external(&external, deployment.Localhost())
3211 .deploy(&mut deployment);
3212
3213 deployment.deploy().await.unwrap();
3214
3215 let mut out = nodes.connect(sum).await;
3216
3217 deployment.start().await.unwrap();
3218
3219 assert_eq!(out.next().await.unwrap(), (2, 204));
3220 }
3221
3222 #[cfg(feature = "deploy")]
3223 #[tokio::test]
3224 async fn reduce_watermark_bounded() {
3225 let mut deployment = Deployment::new();
3226
3227 let mut flow = FlowBuilder::new();
3228 let node = flow.process::<()>();
3229 let external = flow.external::<()>();
3230
3231 let node_tick = node.tick();
3232 let watermark = node_tick.singleton(q!(2));
3233
3234 let sum = node
3235 .source_iter(q!([(0, 100), (1, 101), (2, 102), (2, 102)]))
3236 .into_keyed()
3237 .reduce_watermark(
3238 watermark,
3239 q!(|acc, v| {
3240 *acc += v;
3241 }),
3242 )
3243 .entries()
3244 .send_bincode_external(&external);
3245
3246 let nodes = flow
3247 .with_process(&node, deployment.Localhost())
3248 .with_external(&external, deployment.Localhost())
3249 .deploy(&mut deployment);
3250
3251 deployment.deploy().await.unwrap();
3252
3253 let mut out = nodes.connect(sum).await;
3254
3255 deployment.start().await.unwrap();
3256
3257 assert_eq!(out.next().await.unwrap(), (2, 204));
3258 }
3259
3260 #[cfg(feature = "deploy")]
3261 #[tokio::test]
3262 async fn reduce_watermark_garbage_collect() {
3263 let mut deployment = Deployment::new();
3264
3265 let mut flow = FlowBuilder::new();
3266 let node = flow.process::<()>();
3267 let external = flow.external::<()>();
3268 let (tick_send, tick_trigger) =
3269 node.source_external_bincode::<_, _, _, ExactlyOnce>(&external);
3270
3271 let node_tick = node.tick();
3272 let (watermark_complete_cycle, watermark) =
3273 node_tick.cycle_with_initial(node_tick.singleton(q!(2)));
3274 let next_watermark = watermark.clone().map(q!(|v| v + 1));
3275 watermark_complete_cycle.complete_next_tick(next_watermark);
3276
3277 let tick_triggered_input = node_tick
3278 .singleton(q!((3, 103)))
3279 .into_stream()
3280 .filter_if(
3281 tick_trigger
3282 .clone()
3283 .batch(&node_tick, nondet!(/** test */))
3284 .first()
3285 .is_some(),
3286 )
3287 .all_ticks();
3288
3289 let sum = node
3290 .source_stream(q!(tokio_stream::iter([
3291 (0, 100),
3292 (1, 101),
3293 (2, 102),
3294 (2, 102)
3295 ])))
3296 .merge_unordered(tick_triggered_input)
3297 .into_keyed()
3298 .reduce_watermark(
3299 watermark,
3300 q!(
3301 |acc, v| {
3302 *acc += v;
3303 },
3304 commutative = manual_proof!(/** integer addition is commutative */)
3305 ),
3306 )
3307 .snapshot(&node_tick, nondet!(/** test */))
3308 .entries()
3309 .all_ticks()
3310 .send_bincode_external(&external);
3311
3312 let nodes = flow
3313 .with_default_optimize()
3314 .with_process(&node, deployment.Localhost())
3315 .with_external(&external, deployment.Localhost())
3316 .deploy(&mut deployment);
3317
3318 deployment.deploy().await.unwrap();
3319
3320 let mut tick_send = nodes.connect(tick_send).await;
3321 let mut out_recv = nodes.connect(sum).await;
3322
3323 deployment.start().await.unwrap();
3324
3325 assert_eq!(out_recv.next().await.unwrap(), (2, 204));
3326
3327 tick_send.send(()).await.unwrap();
3328
3329 assert_eq!(out_recv.next().await.unwrap(), (3, 103));
3330 }
3331
3332 #[cfg(feature = "sim")]
3333 #[test]
3334 #[should_panic]
3335 fn sim_batch_nondet_size() {
3336 let mut flow = FlowBuilder::new();
3337 let node = flow.process::<()>();
3338
3339 let input = node.source_iter(q!([(1, 1), (1, 2), (2, 3)])).into_keyed();
3340
3341 let tick = node.tick();
3342 let out_recv = input
3343 .batch(&tick, nondet!(/** test */))
3344 .fold(q!(|| vec![]), q!(|acc, v| acc.push(v)))
3345 .entries()
3346 .all_ticks()
3347 .sim_output();
3348
3349 flow.sim().exhaustive(async || {
3350 out_recv
3351 .assert_yields_only_unordered([(1, vec![1, 2])])
3352 .await;
3353 });
3354 }
3355
3356 #[cfg(feature = "sim")]
3357 #[test]
3358 fn sim_batch_preserves_group_order() {
3359 let mut flow = FlowBuilder::new();
3360 let node = flow.process::<()>();
3361
3362 let input = node.source_iter(q!([(1, 1), (1, 2), (2, 3)])).into_keyed();
3363
3364 let tick = node.tick();
3365 let out_recv = input
3366 .batch(&tick, nondet!(/** test */))
3367 .all_ticks()
3368 .fold_early_stop(
3369 q!(|| 0),
3370 q!(|acc, v| {
3371 *acc = std::cmp::max(v, *acc);
3372 *acc >= 2
3373 }),
3374 )
3375 .entries()
3376 .sim_output();
3377
3378 let instances = flow.sim().exhaustive(async || {
3379 out_recv
3380 .assert_yields_only_unordered([(1, 2), (2, 3)])
3381 .await;
3382 });
3383
3384 assert_eq!(instances, 8);
3385 // (final quiescence checks are free here: the simulation settles deterministically
3386 // once all expected messages have been observed, so no extra instances are explored)
3387 // - three cases: all three in a separate tick (pick where (2, 3) is)
3388 // - two cases: (1, 1) and (1, 2) together, (2, 3) before or after
3389 // - two cases: (1, 1) and (1, 2) separate, (2, 3) grouped with one of them
3390 // - one case: all three together
3391 }
3392
3393 #[cfg(feature = "sim")]
3394 #[test]
3395 fn sim_batch_unordered_shuffles() {
3396 let mut flow = FlowBuilder::new();
3397 let node = flow.process::<()>();
3398
3399 let input = node
3400 .source_iter(q!([(1, 1), (1, 2), (2, 3)]))
3401 .into_keyed()
3402 .weaken_ordering::<NoOrder>();
3403
3404 let tick = node.tick();
3405 let out_recv = input
3406 .batch(&tick, nondet!(/** test */))
3407 .all_ticks()
3408 .entries()
3409 .sim_output();
3410
3411 let instances = flow.sim().exhaustive(async || {
3412 out_recv
3413 .assert_yields_only_unordered([(1, 1), (1, 2), (2, 3)])
3414 .await;
3415 });
3416
3417 assert_eq!(instances, 13);
3418 // - 6 (3 * 2) cases: all three in a separate tick (pick where (2, 3) is), and order of (1, 1), (1, 2)
3419 // - two cases: (1, 1) and (1, 2) together, (2, 3) before or after (order of (1, 1), (1, 2) doesn't matter because batched is still unordered)
3420 // - 4 (2 * 2) cases: (1, 1) and (1, 2) separate, (2, 3) grouped with one of them, and order of (1, 1), (1, 2)
3421 // - one case: all three together (order of (1, 1), (1, 2) doesn't matter because batched is still unordered)
3422 }
3423
3424 #[cfg(feature = "sim")]
3425 #[test]
3426 #[should_panic]
3427 fn sim_observe_order_batched() {
3428 let mut flow = FlowBuilder::new();
3429 let node = flow.process::<()>();
3430
3431 let (port, input) = node.sim_input::<_, NoOrder, _>();
3432
3433 let tick = node.tick();
3434 let batch = input.into_keyed().batch(&tick, nondet!(/** test */));
3435 let out_recv = batch
3436 .assume_ordering::<TotalOrder>(nondet!(/** test */))
3437 .all_ticks()
3438 .first()
3439 .entries()
3440 .sim_output();
3441
3442 flow.sim().exhaustive(async || {
3443 port.send_many_unordered([(1, 1), (1, 2), (2, 1), (2, 2)]);
3444 out_recv
3445 .assert_yields_only_unordered([(1, 1), (2, 1)])
3446 .await; // fails with assume_ordering
3447 });
3448 }
3449
3450 #[cfg(feature = "sim")]
3451 #[test]
3452 fn sim_observe_order_batched_count() {
3453 let mut flow = FlowBuilder::new();
3454 let node = flow.process::<()>();
3455
3456 let (port, input) = node.sim_input::<_, NoOrder, _>();
3457
3458 let tick = node.tick();
3459 let batch = input.into_keyed().batch(&tick, nondet!(/** test */));
3460 let out_recv = batch
3461 .assume_ordering::<TotalOrder>(nondet!(/** test */))
3462 .all_ticks()
3463 .entries()
3464 .sim_output();
3465
3466 let instance_count = flow.sim().exhaustive(async || {
3467 port.send_many_unordered([(1, 1), (1, 2), (2, 1), (2, 2)]);
3468 let _ = out_recv.collect_sorted::<Vec<_>>().await;
3469 });
3470
3471 assert_eq!(instance_count, 104); // too complicated to enumerate here, but less than stream equivalent
3472 }
3473
3474 #[cfg(feature = "sim")]
3475 #[test]
3476 fn sim_top_level_assume_ordering() {
3477 use std::collections::HashMap;
3478
3479 let mut flow = FlowBuilder::new();
3480 let node = flow.process::<()>();
3481
3482 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
3483
3484 let out_recv = input
3485 .into_keyed()
3486 .assume_ordering::<TotalOrder>(nondet!(/** test */))
3487 .fold_early_stop(
3488 q!(|| Vec::new()),
3489 q!(|acc, v| {
3490 acc.push(v);
3491 acc.len() >= 2
3492 }),
3493 )
3494 .entries()
3495 .sim_output();
3496
3497 let instance_count = flow.sim().exhaustive(async || {
3498 in_send.send_many_unordered([(1, 'a'), (1, 'b'), (2, 'c'), (2, 'd')]);
3499 let out: HashMap<_, _> = out_recv
3500 .collect_sorted::<Vec<_>>()
3501 .await
3502 .into_iter()
3503 .collect();
3504 // Each key accumulates its values; we get one entry per key
3505 assert_eq!(out.len(), 2);
3506 });
3507
3508 assert_eq!(instance_count, 24)
3509 }
3510
3511 #[cfg(feature = "sim")]
3512 #[test]
3513 fn sim_top_level_assume_ordering_cycle_back() {
3514 use std::collections::HashMap;
3515
3516 let mut flow = FlowBuilder::new();
3517 let node = flow.process::<()>();
3518 let node2 = flow.process::<()>();
3519
3520 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
3521
3522 let (complete_cycle_back, cycle_back) =
3523 node.forward_ref::<super::KeyedStream<_, _, _, _, NoOrder>>();
3524 let ordered = input
3525 .into_keyed()
3526 .merge_unordered(cycle_back)
3527 .assume_ordering::<TotalOrder>(nondet!(/** test */));
3528 complete_cycle_back.complete(
3529 ordered
3530 .clone()
3531 .map(q!(|v| v + 1))
3532 .filter(q!(|v| v % 2 == 1))
3533 .entries()
3534 .send(&node2, TCP.fail_stop().bincode())
3535 .send(&node, TCP.fail_stop().bincode())
3536 .into_keyed(),
3537 );
3538
3539 let out_recv = ordered
3540 .fold_early_stop(
3541 q!(|| Vec::new()),
3542 q!(|acc, v| {
3543 acc.push(v);
3544 acc.len() >= 2
3545 }),
3546 )
3547 .entries()
3548 .sim_output();
3549
3550 let mut saw = false;
3551 let instance_count = flow.sim().exhaustive(async || {
3552 // Send (1, 0) and (1, 2). 0+1=1 is odd so cycles back.
3553 // We want to see [0, 1] - the cycled back value interleaved
3554 in_send.send_many_unordered([(1, 0), (1, 2)]);
3555 let out: HashMap<_, _> = out_recv
3556 .collect_sorted::<Vec<_>>()
3557 .await
3558 .into_iter()
3559 .collect();
3560
3561 // We want to see an instance where key 1 gets: 0, then 1 (cycled back from 0+1)
3562 if let Some(values) = out.get(&1)
3563 && *values == vec![0, 1]
3564 {
3565 saw = true;
3566 }
3567 });
3568
3569 assert!(
3570 saw,
3571 "did not see an instance with key 1 having [0, 1] in order"
3572 );
3573 assert_eq!(instance_count, 6);
3574 }
3575
3576 #[cfg(feature = "sim")]
3577 #[test]
3578 fn sim_top_level_assume_ordering_cross_key_cycle() {
3579 use std::collections::HashMap;
3580
3581 // This test demonstrates why releasing one entry at a time is important:
3582 // When one key's observed order cycles back into a different key, we need
3583 // to be able to interleave the cycled-back entry with pending items for
3584 // that other key.
3585 let mut flow = FlowBuilder::new();
3586 let node = flow.process::<()>();
3587 let node2 = flow.process::<()>();
3588
3589 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
3590
3591 let (complete_cycle_back, cycle_back) =
3592 node.forward_ref::<super::KeyedStream<_, _, _, _, NoOrder>>();
3593 let ordered = input
3594 .into_keyed()
3595 .merge_unordered(cycle_back)
3596 .assume_ordering::<TotalOrder>(nondet!(/** test */));
3597
3598 // Cycle back: when we see (1, 10), emit (2, 100) to key 2
3599 complete_cycle_back.complete(
3600 ordered
3601 .clone()
3602 .filter(q!(|v| *v == 10))
3603 .map(q!(|_| 100))
3604 .entries()
3605 .map(q!(|(_, v)| (2, v))) // Change key from 1 to 2
3606 .send(&node2, TCP.fail_stop().bincode())
3607 .send(&node, TCP.fail_stop().bincode())
3608 .into_keyed(),
3609 );
3610
3611 let out_recv = ordered
3612 .fold_early_stop(
3613 q!(|| Vec::new()),
3614 q!(|acc, v| {
3615 acc.push(v);
3616 acc.len() >= 2
3617 }),
3618 )
3619 .entries()
3620 .sim_output();
3621
3622 // We want to see an instance where:
3623 // - (1, 10) is released first
3624 // - This causes (2, 100) to be cycled back
3625 // - (2, 100) is released BEFORE (2, 20) which was already pending
3626 let mut saw_cross_key_interleave = false;
3627 let instance_count = flow.sim().exhaustive(async || {
3628 // Send (1, 10), (1, 11) for key 1, and (2, 20), (2, 21) for key 2
3629 in_send.send_many_unordered([(1, 10), (1, 11), (2, 20), (2, 21)]);
3630 let out: HashMap<_, _> = out_recv
3631 .collect_sorted::<Vec<_>>()
3632 .await
3633 .into_iter()
3634 .collect();
3635
3636 // Check if we see the cross-key interleaving:
3637 // key 2 should have [100, 20] or [100, 21] - cycled back 100 before a pending item
3638 if let Some(values) = out.get(&2)
3639 && values.len() >= 2
3640 && values[0] == 100
3641 {
3642 saw_cross_key_interleave = true;
3643 }
3644 });
3645
3646 assert!(
3647 saw_cross_key_interleave,
3648 "did not see an instance where cycled-back 100 was released before pending items for key 2"
3649 );
3650 assert_eq!(instance_count, 60);
3651 }
3652
3653 #[cfg(feature = "sim")]
3654 #[test]
3655 fn sim_top_level_assume_ordering_cycle_back_tick() {
3656 use std::collections::HashMap;
3657
3658 let mut flow = FlowBuilder::new();
3659 let node = flow.process::<()>();
3660 let node2 = flow.process::<()>();
3661
3662 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
3663
3664 let (complete_cycle_back, cycle_back) =
3665 node.forward_ref::<super::KeyedStream<_, _, _, _, NoOrder>>();
3666 let ordered = input
3667 .into_keyed()
3668 .merge_unordered(cycle_back)
3669 .assume_ordering::<TotalOrder>(nondet!(/** test */));
3670 complete_cycle_back.complete(
3671 ordered
3672 .clone()
3673 .batch(&node.tick(), nondet!(/** test */))
3674 .all_ticks()
3675 .map(q!(|v| v + 1))
3676 .filter(q!(|v| v % 2 == 1))
3677 .entries()
3678 .send(&node2, TCP.fail_stop().bincode())
3679 .send(&node, TCP.fail_stop().bincode())
3680 .into_keyed(),
3681 );
3682
3683 let out_recv = ordered
3684 .fold_early_stop(
3685 q!(|| Vec::new()),
3686 q!(|acc, v| {
3687 acc.push(v);
3688 acc.len() >= 2
3689 }),
3690 )
3691 .entries()
3692 .sim_output();
3693
3694 let mut saw = false;
3695 let instance_count = flow.sim().exhaustive(async || {
3696 in_send.send_many_unordered([(1, 0), (1, 2)]);
3697 let out: HashMap<_, _> = out_recv
3698 .collect_sorted::<Vec<_>>()
3699 .await
3700 .into_iter()
3701 .collect();
3702
3703 if let Some(values) = out.get(&1)
3704 && *values == vec![0, 1]
3705 {
3706 saw = true;
3707 }
3708 });
3709
3710 assert!(
3711 saw,
3712 "did not see an instance with key 1 having [0, 1] in order"
3713 );
3714 assert_eq!(instance_count, 58);
3715 }
3716
3717 #[cfg(feature = "sim")]
3718 #[test]
3719 fn sim_entries_partially_ordered_bounded() {
3720 let mut flow = FlowBuilder::new();
3721 let node = flow.process::<()>();
3722
3723 let (port, input) = node.sim_input::<_, TotalOrder, _>();
3724
3725 let tick = node.tick();
3726 let batch = input.into_keyed().batch(&tick, nondet!(/** test */));
3727 let out_recv = batch
3728 .entries_partially_ordered(nondet!(/** test */))
3729 .all_ticks()
3730 .sim_output();
3731
3732 let instance_count = flow.sim().exhaustive(async || {
3733 port.send((1, 'a'));
3734 port.send((1, 'b'));
3735 port.send((2, 'c'));
3736 let _: Vec<(i32, char)> = out_recv.collect().await;
3737 });
3738
3739 assert_eq!(instance_count, 12);
3740 }
3741
3742 #[cfg(feature = "sim")]
3743 #[test]
3744 fn sim_entries_partially_ordered_top_level() {
3745 let mut flow = FlowBuilder::new();
3746 let node = flow.process::<()>();
3747
3748 let (in_send, input) = node.sim_input::<_, TotalOrder, _>();
3749
3750 let out_recv = input
3751 .into_keyed()
3752 .entries_partially_ordered(nondet!(/** test */))
3753 .sim_output();
3754
3755 let instance_count = flow.sim().exhaustive(async || {
3756 in_send.send((1, 'a'));
3757 in_send.send((1, 'b'));
3758 in_send.send((2, 'c'));
3759 let _: Vec<(i32, char)> = out_recv.collect().await;
3760 });
3761
3762 assert_eq!(instance_count, 3);
3763 }
3764
3765 #[cfg(feature = "sim")]
3766 #[test]
3767 fn sim_entries_partially_ordered_cycle_back() {
3768 let mut flow = FlowBuilder::new();
3769 let node = flow.process::<()>();
3770 let node2 = flow.process::<()>();
3771
3772 let (in_send, input) = node.sim_input::<_, NoOrder, _>();
3773
3774 let (complete_cycle_back, cycle_back) =
3775 node.forward_ref::<super::KeyedStream<_, _, _, _, NoOrder>>();
3776 let ordered = input
3777 .into_keyed()
3778 .merge_unordered(cycle_back)
3779 .assume_ordering::<TotalOrder>(nondet!(/** test */));
3780
3781 let flat = ordered
3782 .clone()
3783 .entries_partially_ordered(nondet!(/** test */));
3784
3785 complete_cycle_back.complete(
3786 flat.clone()
3787 .map(q!(|(k, v): (i32, i32)| (k, v + 1)))
3788 .filter(q!(|(_, v)| *v % 2 == 1))
3789 .send(&node2, TCP.fail_stop().bincode())
3790 .send(&node, TCP.fail_stop().bincode())
3791 .into_keyed(),
3792 );
3793
3794 let out_recv = flat.sim_output();
3795
3796 let mut saw = false;
3797 let instance_count = flow.sim().exhaustive(async || {
3798 // Send (1, 0) and (1, 2). 0+1=1 is odd so cycles back as (1, 1).
3799 // We want to see (1, 1) before (1, 2) - the cycled back value beats the pending one
3800 in_send.send_many_unordered([(1, 0), (1, 2)]);
3801 let results: Vec<(i32, i32)> = out_recv.collect().await;
3802
3803 let pos_1 = results.iter().position(|v| *v == (1, 1));
3804 let pos_2 = results.iter().position(|v| *v == (1, 2));
3805 if let (Some(p1), Some(p2)) = (pos_1, pos_2)
3806 && p1 < p2
3807 {
3808 saw = true;
3809 }
3810 });
3811
3812 assert!(saw, "did not see an instance with (1, 1) before (1, 2)");
3813 assert_eq!(instance_count, 78);
3814 }
3815
3816 /// Tests that `merge_ordered` on a keyed stream explores every valid
3817 /// interleaving within a shared key while always preserving per-input
3818 /// order.
3819 #[cfg(feature = "sim")]
3820 #[test]
3821 fn sim_keyed_merge_ordered() {
3822 let mut flow = FlowBuilder::new();
3823 let node = flow.process::<()>();
3824
3825 let (in_send, input) = node.sim_input::<_, TotalOrder, _>();
3826 let (in_send2, input2) = node.sim_input::<_, TotalOrder, _>();
3827
3828 let out_recv = input
3829 .into_keyed()
3830 .merge_ordered(input2.into_keyed(), nondet!(/** test */))
3831 .entries_partially_ordered(nondet!(/** test */))
3832 .sim_output();
3833
3834 let mut saw_first = false;
3835 let mut saw_interleaved = false;
3836 let mut saw_second_first = false;
3837 let instances = flow.sim().exhaustive(async || {
3838 in_send.send((1, 'a'));
3839 in_send.send((1, 'b'));
3840 in_send2.send((1, 'c'));
3841
3842 let out: Vec<(i32, char)> = out_recv.collect().await;
3843 let key1: Vec<char> = out
3844 .iter()
3845 .filter(|(k, _)| *k == 1)
3846 .map(|(_, v)| *v)
3847 .collect();
3848
3849 // Within-group order for the first input must always be preserved.
3850 let first_order: Vec<char> = key1
3851 .iter()
3852 .filter(|c| **c == 'a' || **c == 'b')
3853 .copied()
3854 .collect();
3855 assert_eq!(
3856 first_order,
3857 vec!['a', 'b'],
3858 "within-group order violated: {:?}",
3859 out
3860 );
3861
3862 match key1.as_slice() {
3863 ['a', 'b', 'c'] => saw_first = true,
3864 ['a', 'c', 'b'] => saw_interleaved = true,
3865 ['c', 'a', 'b'] => saw_second_first = true,
3866 other => panic!("unexpected interleaving: {:?}", other),
3867 }
3868 });
3869
3870 assert!(saw_first, "did not observe [a, b, c]");
3871 assert!(saw_interleaved, "did not observe [a, c, b]");
3872 assert!(saw_second_first, "did not observe [c, a, b]");
3873 assert_eq!(instances, 33);
3874 }
3875
3876 /// Tests that `merge_ordered` on a keyed stream interleaves each group
3877 /// *independently*. It must be possible to observe, in the same execution,
3878 /// key `10` taking its second-input value before its first-input value
3879 /// while key `20` does the opposite. A merge that treated the two inputs
3880 /// as a single totally-ordered sequence could not produce this combination.
3881 #[cfg(feature = "sim")]
3882 #[test]
3883 fn sim_keyed_merge_ordered_independent_keys() {
3884 let mut flow = FlowBuilder::new();
3885 let node = flow.process::<()>();
3886
3887 let (in_send, input) = node.sim_input::<_, TotalOrder, _>();
3888 let (in_send2, input2) = node.sim_input::<_, TotalOrder, _>();
3889
3890 let out_recv = input
3891 .into_keyed()
3892 .merge_ordered(input2.into_keyed(), nondet!(/** test */))
3893 .entries_partially_ordered(nondet!(/** test */))
3894 .sim_output();
3895
3896 let mut saw_independent = false;
3897 let instances = flow.sim().exhaustive(async || {
3898 // First input: key 10 -> [1], key 20 -> [2].
3899 in_send.send((10, 1));
3900 in_send.send((20, 2));
3901 // Second input: key 10 -> [4], key 20 -> [3].
3902 in_send2.send((10, 4));
3903 in_send2.send((20, 3));
3904
3905 let out: Vec<(i32, i32)> = out_recv.collect().await;
3906 let key10: Vec<i32> = out
3907 .iter()
3908 .filter(|(k, _)| *k == 10)
3909 .map(|(_, v)| *v)
3910 .collect();
3911 let key20: Vec<i32> = out
3912 .iter()
3913 .filter(|(k, _)| *k == 20)
3914 .map(|(_, v)| *v)
3915 .collect();
3916
3917 // Within-input order must be preserved within each key (each key has
3918 // a single value per input here, so only the multiset is checked).
3919 let mut s10 = key10.clone();
3920 s10.sort();
3921 assert_eq!(s10, vec![1, 4], "unexpected values for key 10: {:?}", out);
3922 let mut s20 = key20.clone();
3923 s20.sort();
3924 assert_eq!(s20, vec![2, 3], "unexpected values for key 20: {:?}", out);
3925
3926 // key 10: second-input value (4) before first-input value (1).
3927 // key 20: first-input value (2) before second-input value (3).
3928 if key10 == vec![4, 1] && key20 == vec![2, 3] {
3929 saw_independent = true;
3930 }
3931 });
3932
3933 assert!(
3934 saw_independent,
3935 "did not observe per-key-independent interleaving"
3936 );
3937 assert_eq!(instances, 2944);
3938 }
3939}