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