hydro_lang/live_collections/keyed_singleton.rs
1//! Definitions for the [`KeyedSingleton`] 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 sealed::sealed;
11use stageleft::{IntoQuotedMut, QuotedWithContext, q};
12
13use super::OperatorContext;
14use super::boundedness::{Bounded, Boundedness, IsBounded, Unbounded};
15use super::keyed_stream::KeyedStream;
16use super::optional::Optional;
17use super::singleton::Singleton;
18use super::sliced::sliced;
19use super::stream::{ExactlyOnce, NoOrder, Stream, TotalOrder};
20use crate::compile::builder::{CycleId, FlowState};
21use crate::compile::ir::{
22 CollectionKind, HydroIrOpMetadata, HydroNode, HydroRoot, KeyedSingletonBoundKind, SharedNode,
23};
24#[cfg(stageleft_runtime)]
25use crate::forward_handle::{CycleCollection, ReceiverComplete};
26use crate::forward_handle::{ForwardRef, TickCycle};
27use crate::live_collections::stream::{Ordering, Retries};
28#[cfg(stageleft_runtime)]
29use crate::location::dynamic::{DynLocation, LocationId};
30use crate::location::tick::DeferTick;
31use crate::location::{Atomic, Location, Tick, check_matching_location};
32use crate::manual_expr::ManualExpr;
33use crate::nondet::{NonDet, nondet};
34use crate::properties::manual_proof;
35
36/// A marker trait indicating which components of a [`KeyedSingleton`] may change.
37///
38/// In addition to [`Bounded`] (all entries are fixed) and [`Unbounded`] (entries may be added /
39/// changed, but not removed), this also includes an additional variant [`BoundedValue`], which
40/// indicates that entries may be added over time, but once an entry is added it will never be
41/// removed and its value will never change.
42pub trait KeyedSingletonBound {
43 /// The [`Boundedness`] of the [`Stream`] underlying the keyed singleton.
44 type UnderlyingBound: Boundedness;
45 /// The [`Boundedness`] of each entry's value; [`Bounded`] means it is immutable.
46 type ValueBound: Boundedness;
47
48 /// The type of the keyed singleton if the value for each key is immutable.
49 type WithBoundedValue: KeyedSingletonBound<
50 UnderlyingBound = Self::UnderlyingBound,
51 ValueBound = Bounded,
52 EraseMonotonic = Self::WithBoundedValue,
53 >;
54
55 /// The [`Boundedness`] of this [`Singleton`] if it is produced from a [`KeyedStream`] with [`Self`] boundedness.
56 type KeyedStreamToMonotone: KeyedSingletonBound<UnderlyingBound = Self::UnderlyingBound, ValueBound = Self::ValueBound>;
57
58 /// The [`Boundedness`] of the keyed singleton produced by folding a [`KeyedStream`] with
59 /// [`Self`] boundedness when the aggregation does *not* have a monotonicity proof.
60 ///
61 /// Without a monotonicity proof, the per-key values may change arbitrarily, so an unbounded
62 /// input collapses to [`MonotonicKeys`] (keys are still only added, never removed).
63 type KeyedStreamToNonMonotone: KeyedSingletonBound<UnderlyingBound = Self::UnderlyingBound, ValueBound = Self::ValueBound>;
64
65 /// The type of the keyed singleton if the value for each key is no longer monotonic.
66 type EraseMonotonic: KeyedSingletonBound<UnderlyingBound = Self::UnderlyingBound, ValueBound = Self::ValueBound>;
67
68 /// Returns the [`KeyedSingletonBoundKind`] corresponding to this type.
69 fn bound_kind() -> KeyedSingletonBoundKind;
70}
71
72impl KeyedSingletonBound for Unbounded {
73 type UnderlyingBound = Unbounded;
74 type ValueBound = Unbounded;
75 type WithBoundedValue = BoundedValue;
76 type KeyedStreamToMonotone = MonotonicValue;
77 type KeyedStreamToNonMonotone = MonotonicKeys;
78 type EraseMonotonic = Unbounded;
79
80 fn bound_kind() -> KeyedSingletonBoundKind {
81 KeyedSingletonBoundKind::Unbounded
82 }
83}
84
85impl KeyedSingletonBound for Bounded {
86 type UnderlyingBound = Bounded;
87 type ValueBound = Bounded;
88 type WithBoundedValue = Bounded;
89 type KeyedStreamToMonotone = Bounded;
90 type KeyedStreamToNonMonotone = Bounded;
91 type EraseMonotonic = Bounded;
92
93 fn bound_kind() -> KeyedSingletonBoundKind {
94 KeyedSingletonBoundKind::Bounded
95 }
96}
97
98/// A variation of boundedness specific to [`KeyedSingleton`], which indicates that once a key appears,
99/// its value is bounded and will never change, but new entries may appear asynchronously
100pub struct BoundedValue;
101
102impl KeyedSingletonBound for BoundedValue {
103 type UnderlyingBound = Unbounded;
104 type ValueBound = Bounded;
105 type WithBoundedValue = BoundedValue;
106 type KeyedStreamToMonotone = BoundedValue;
107 type KeyedStreamToNonMonotone = BoundedValue;
108 type EraseMonotonic = BoundedValue;
109
110 fn bound_kind() -> KeyedSingletonBoundKind {
111 KeyedSingletonBoundKind::BoundedValue
112 }
113}
114
115/// A variation of boundedness specific to [`KeyedSingleton`], which indicates that once a key appears,
116/// it will never be removed, and the corresponding value will only increase monotonically.
117pub struct MonotonicValue;
118
119impl KeyedSingletonBound for MonotonicValue {
120 type UnderlyingBound = Unbounded;
121 type ValueBound = Unbounded;
122 type WithBoundedValue = BoundedValue;
123 type KeyedStreamToMonotone = MonotonicValue;
124 type KeyedStreamToNonMonotone = MonotonicKeys;
125 type EraseMonotonic = MonotonicKeys;
126
127 fn bound_kind() -> KeyedSingletonBoundKind {
128 KeyedSingletonBoundKind::MonotonicValue
129 }
130}
131
132/// A variation of boundedness specific to [`KeyedSingleton`], which indicates that once a key
133/// appears, it will never be removed, but the corresponding value may change arbitrarily.
134pub struct MonotonicKeys;
135
136impl KeyedSingletonBound for MonotonicKeys {
137 type UnderlyingBound = Unbounded;
138 type ValueBound = Unbounded;
139 type WithBoundedValue = BoundedValue;
140 type KeyedStreamToMonotone = MonotonicKeys;
141 type KeyedStreamToNonMonotone = MonotonicKeys;
142 type EraseMonotonic = MonotonicKeys;
143
144 fn bound_kind() -> KeyedSingletonBoundKind {
145 KeyedSingletonBoundKind::MonotonicKeys
146 }
147}
148
149#[sealed]
150#[diagnostic::on_unimplemented(
151 message = "The keyed singleton must have monotonic values (`MonotonicValue`) or be bounded (`Bounded`), but has bound `{Self}`. Strengthen the monotonicity upstream or consider a different API.",
152 label = "required here",
153 note = "To intentionally process a non-deterministic snapshot or batch, you may want to use a `sliced!` region. This introduces non-determinism so avoid unless necessary."
154)]
155/// Marker trait that is implemented for [`KeyedSingletonBound`] types whose per-key values
156/// are monotonically non-decreasing (or bounded).
157pub trait IsKeyedMonotonic: KeyedSingletonBound {}
158
159#[sealed]
160#[diagnostic::do_not_recommend]
161impl IsKeyedMonotonic for MonotonicValue {}
162
163#[sealed]
164#[diagnostic::do_not_recommend]
165impl IsKeyedMonotonic for BoundedValue {}
166
167#[sealed]
168#[diagnostic::do_not_recommend]
169impl<B: IsBounded + KeyedSingletonBound> IsKeyedMonotonic for B {}
170
171/// Mapping from keys of type `K` to values of type `V`.
172///
173/// Keyed Singletons capture an asynchronously updated mapping from keys of the `K` to values of
174/// type `V`, where the order of keys is non-deterministic. In addition to the standard boundedness
175/// variants ([`Bounded`] for finite and immutable, [`Unbounded`] for asynchronously changing),
176/// keyed singletons can use [`BoundedValue`] to declare that new keys may be added over time, but
177/// keys cannot be removed and the value for each key is immutable.
178///
179/// Type Parameters:
180/// - `K`: the type of the key for each entry
181/// - `V`: the type of the value for each entry
182/// - `Loc`: the [`Location`] where the keyed singleton is materialized
183/// - `Bound`: tracks whether the entries are:
184/// - [`Bounded`] (local and finite)
185/// - [`Unbounded`] (asynchronous with entries added / removed / changed over time)
186/// - [`BoundedValue`] (asynchronous with immutable values for each key and no removals)
187pub struct KeyedSingleton<K, V, Loc, Bound: KeyedSingletonBound> {
188 pub(crate) location: Loc,
189 pub(crate) ir_node: Rc<RefCell<HydroNode>>,
190 pub(crate) flow_state: FlowState,
191
192 _phantom: PhantomData<(K, V, Loc, Bound)>,
193}
194
195impl<K, V, L, B: KeyedSingletonBound> Drop for KeyedSingleton<K, V, L, B> {
196 fn drop(&mut self) {
197 let ir_node = self.ir_node.replace(HydroNode::Placeholder);
198 if !matches!(ir_node, HydroNode::Placeholder) && !ir_node.is_shared_with_others() {
199 self.flow_state.borrow_mut().try_push_root(HydroRoot::Null {
200 input: Box::new(ir_node),
201 op_metadata: HydroIrOpMetadata::new(),
202 });
203 }
204 }
205}
206
207impl<'a, K: Clone, V: Clone, Loc: Location<'a>, Bound: KeyedSingletonBound> Clone
208 for KeyedSingleton<K, V, Loc, Bound>
209{
210 fn clone(&self) -> Self {
211 if !matches!(self.ir_node.borrow().deref(), HydroNode::Tee { .. }) {
212 let orig_ir_node = self.ir_node.replace(HydroNode::Placeholder);
213 *self.ir_node.borrow_mut() = HydroNode::Tee {
214 inner: SharedNode(Rc::new(RefCell::new(orig_ir_node))),
215 metadata: self.location.new_node_metadata(Self::collection_kind()),
216 };
217 }
218
219 if let HydroNode::Tee { inner, metadata } = self.ir_node.borrow().deref() {
220 KeyedSingleton {
221 location: self.location.clone(),
222 flow_state: self.flow_state.clone(),
223 ir_node: super::tracked_ir_node(
224 &self.flow_state,
225 HydroNode::Tee {
226 inner: SharedNode(inner.0.clone()),
227 metadata: metadata.clone(),
228 },
229 ),
230 _phantom: PhantomData,
231 }
232 } else {
233 unreachable!()
234 }
235 }
236}
237
238impl<'a, K, V, L, B: KeyedSingletonBound> CycleCollection<'a, ForwardRef>
239 for KeyedSingleton<K, V, L, B>
240where
241 L: Location<'a>,
242{
243 type Location = L;
244
245 fn create_source(cycle_id: CycleId, location: L) -> Self {
246 let flow_state = location.flow_state().clone();
247 KeyedSingleton {
248 ir_node: super::tracked_ir_node(
249 &flow_state,
250 HydroNode::CycleSource {
251 cycle_id,
252 metadata: location.new_node_metadata(Self::collection_kind()),
253 },
254 ),
255 flow_state,
256 location,
257 _phantom: PhantomData,
258 }
259 }
260}
261
262impl<'a, K, V, L> CycleCollection<'a, TickCycle> for KeyedSingleton<K, V, Tick<L>, Bounded>
263where
264 L: Location<'a>,
265{
266 type Location = Tick<L>;
267
268 fn create_source(cycle_id: CycleId, location: Tick<L>) -> Self {
269 KeyedSingleton::new(
270 location.clone(),
271 HydroNode::CycleSource {
272 cycle_id,
273 metadata: location.new_node_metadata(Self::collection_kind()),
274 },
275 )
276 }
277}
278
279impl<'a, K, V, L> DeferTick for KeyedSingleton<K, V, Tick<L>, Bounded>
280where
281 L: Location<'a>,
282{
283 fn defer_tick(self) -> Self {
284 KeyedSingleton::defer_tick(self)
285 }
286}
287
288impl<'a, K, V, L, B: KeyedSingletonBound> ReceiverComplete<'a, ForwardRef>
289 for KeyedSingleton<K, V, L, B>
290where
291 L: Location<'a>,
292{
293 fn complete(self, cycle_id: CycleId, expected_location: LocationId) {
294 assert_eq!(
295 Location::id(&self.location),
296 expected_location,
297 "locations do not match"
298 );
299 self.location
300 .flow_state()
301 .borrow_mut()
302 .push_root(HydroRoot::CycleSink {
303 cycle_id,
304 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
305 op_metadata: HydroIrOpMetadata::new(),
306 });
307 }
308}
309
310impl<'a, K, V, L> ReceiverComplete<'a, TickCycle> for KeyedSingleton<K, V, Tick<L>, Bounded>
311where
312 L: Location<'a>,
313{
314 fn complete(self, cycle_id: CycleId, expected_location: LocationId) {
315 assert_eq!(
316 Location::id(&self.location),
317 expected_location,
318 "locations do not match"
319 );
320 self.location
321 .flow_state()
322 .borrow_mut()
323 .push_root(HydroRoot::CycleSink {
324 cycle_id,
325 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
326 op_metadata: HydroIrOpMetadata::new(),
327 });
328 }
329}
330
331impl<'a, K, V, L: Location<'a>, B: KeyedSingletonBound> KeyedSingleton<K, V, L, B> {
332 pub(crate) fn new(location: L, ir_node: HydroNode) -> Self {
333 debug_assert_eq!(ir_node.metadata().location_id, Location::id(&location));
334 debug_assert_eq!(ir_node.metadata().collection_kind, Self::collection_kind());
335
336 let flow_state = location.flow_state().clone();
337 let ir_node = super::tracked_ir_node(&flow_state, ir_node);
338 KeyedSingleton {
339 location,
340 flow_state,
341 ir_node,
342 _phantom: PhantomData,
343 }
344 }
345
346 /// Returns the [`Location`] where this keyed singleton is being materialized.
347 pub fn location(&self) -> &L {
348 &self.location
349 }
350
351 /// Weakens the consistency of this live collection to not guarantee any consistency across
352 /// cluster members (if this collection is on a cluster).
353 pub fn weaken_consistency(self) -> KeyedSingleton<K, V, L::DropConsistency, B>
354 where
355 L: Location<'a>,
356 {
357 if L::consistency()
358 .is_none_or(|c| c == crate::location::dynamic::ClusterConsistency::NoConsistency)
359 {
360 // already no consistency
361 KeyedSingleton::new(
362 self.location.drop_consistency(),
363 self.ir_node.replace(HydroNode::Placeholder),
364 )
365 } else {
366 KeyedSingleton::new(
367 self.location.drop_consistency(),
368 HydroNode::Cast {
369 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
370 metadata: self
371 .location
372 .drop_consistency()
373 .new_node_metadata(
374 KeyedSingleton::<K, V, L::DropConsistency, B>::collection_kind(),
375 ),
376 },
377 )
378 }
379 }
380
381 /// Casts this live collection to have the consistency guarantees specified in the given
382 /// location type parameter. The developer must ensure that the strengthened consistency
383 /// is actually guaranteed, via the proof field (see [`crate::prelude::manual_proof`]).
384 pub fn assert_has_consistency_of<L2: Location<'a, DropConsistency = L::DropConsistency>>(
385 self,
386 _proof: impl crate::properties::ConsistencyProof,
387 ) -> KeyedSingleton<K, V, L2, B>
388 where
389 L: Location<'a>,
390 {
391 if L::consistency() == L2::consistency() {
392 // already consistent
393 KeyedSingleton::new(
394 self.location.with_consistency_of(),
395 self.ir_node.replace(HydroNode::Placeholder),
396 )
397 } else {
398 KeyedSingleton::new(
399 self.location.with_consistency_of(),
400 HydroNode::AssertIsConsistent {
401 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
402 trusted: false,
403 metadata: self
404 .location
405 .clone()
406 .with_consistency_of::<L2>()
407 .new_node_metadata(KeyedSingleton::<K, V, L2, B>::collection_kind()),
408 },
409 )
410 }
411 }
412}
413
414#[cfg(stageleft_runtime)]
415fn key_count_inside_tick<'a, K, V, L: Location<'a>>(
416 me: KeyedSingleton<K, V, L, Bounded>,
417) -> Singleton<usize, L, Bounded> {
418 me.entries().count()
419}
420
421#[cfg(stageleft_runtime)]
422fn into_singleton_inside_tick<'a, K, V, L: Location<'a>>(
423 me: KeyedSingleton<K, V, L, Bounded>,
424) -> Singleton<HashMap<K, V>, L, Bounded>
425where
426 K: Eq + Hash,
427{
428 me.entries()
429 .assume_ordering_trusted(nondet!(
430 /// There is only one element associated with each key. The closure technically
431 /// isn't commutative in the case where both passed entries have the same key
432 /// but different values.
433 ///
434 /// In the future, we may want to have an `assume!(...)` statement in the UDF that
435 /// the key is never already present in the map.
436 ))
437 .fold(
438 q!(|| HashMap::new()),
439 q!(|map, (k, v)| {
440 map.insert(k, v);
441 }),
442 )
443}
444
445impl<'a, K, V, L: Location<'a>, B: KeyedSingletonBound> KeyedSingleton<K, V, L, B> {
446 pub(crate) fn collection_kind() -> CollectionKind {
447 CollectionKind::KeyedSingleton {
448 bound: B::bound_kind(),
449 key_type: stageleft::quote_type::<K>().into(),
450 value_type: stageleft::quote_type::<V>().into(),
451 }
452 }
453
454 /// Transforms each value by invoking `f` on each element, with keys staying the same
455 /// after transformation. If you need access to the key, see [`KeyedSingleton::map_with_key`].
456 ///
457 /// If you do not want to modify the stream and instead only want to view
458 /// each item use [`KeyedSingleton::inspect`] instead.
459 ///
460 /// # Example
461 /// ```rust
462 /// # #[cfg(feature = "deploy")] {
463 /// # use hydro_lang::prelude::*;
464 /// # use futures::StreamExt;
465 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
466 /// let keyed_singleton = // { 1: 2, 2: 4 }
467 /// # process
468 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
469 /// # .into_keyed()
470 /// # .first();
471 /// keyed_singleton.map(q!(|v| v + 1))
472 /// # .entries()
473 /// # }, |mut stream| async move {
474 /// // { 1: 3, 2: 5 }
475 /// # let mut results = Vec::new();
476 /// # for _ in 0..2 {
477 /// # results.push(stream.next().await.unwrap());
478 /// # }
479 /// # results.sort();
480 /// # assert_eq!(results, vec![(1, 3), (2, 5)]);
481 /// # }));
482 /// # }
483 /// ```
484 pub fn map<U, F>(
485 self,
486 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>> + Copy,
487 ) -> KeyedSingleton<K, U, L, B::EraseMonotonic>
488 where
489 F: Fn(V) -> U + 'a,
490 {
491 let f: ManualExpr<F, _> =
492 ManualExpr::new(move |ctx: &OperatorContext<L, B::UnderlyingBound>| {
493 f.splice_fn1_ctx(ctx)
494 });
495 let map_f = q!({
496 let orig = f;
497 move |(k, v)| (k, orig(v))
498 })
499 .splice_fn1_ctx::<(K, V), (K, U)>(&OperatorContext::<L, B::UnderlyingBound>::new(
500 &self.location,
501 ))
502 .into();
503
504 KeyedSingleton::new(
505 self.location.clone(),
506 HydroNode::Map {
507 f: map_f,
508 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
509 metadata: self.location.new_node_metadata(KeyedSingleton::<
510 K,
511 U,
512 L,
513 B::EraseMonotonic,
514 >::collection_kind()),
515 },
516 )
517 }
518
519 /// Transforms each value by invoking `f` on each key-value pair, with keys staying the same
520 /// after transformation. Unlike [`KeyedSingleton::map`], this gives access to both the key and value.
521 ///
522 /// The closure `f` receives a tuple `(K, V)` containing both the key and value, and returns
523 /// the new value `U`. The key remains unchanged in the output.
524 ///
525 /// # Example
526 /// ```rust
527 /// # #[cfg(feature = "deploy")] {
528 /// # use hydro_lang::prelude::*;
529 /// # use futures::StreamExt;
530 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
531 /// let keyed_singleton = // { 1: 2, 2: 4 }
532 /// # process
533 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
534 /// # .into_keyed()
535 /// # .first();
536 /// keyed_singleton.map_with_key(q!(|(k, v)| k + v))
537 /// # .entries()
538 /// # }, |mut stream| async move {
539 /// // { 1: 3, 2: 6 }
540 /// # let mut results = Vec::new();
541 /// # for _ in 0..2 {
542 /// # results.push(stream.next().await.unwrap());
543 /// # }
544 /// # results.sort();
545 /// # assert_eq!(results, vec![(1, 3), (2, 6)]);
546 /// # }));
547 /// # }
548 /// ```
549 pub fn map_with_key<U, F>(
550 self,
551 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>> + Copy,
552 ) -> KeyedSingleton<K, U, L, B::EraseMonotonic>
553 where
554 F: Fn((K, V)) -> U + 'a,
555 K: Clone,
556 {
557 let f: ManualExpr<F, _> =
558 ManualExpr::new(move |ctx: &OperatorContext<L, B::UnderlyingBound>| {
559 f.splice_fn1_ctx(ctx)
560 });
561 let map_f = q!({
562 let orig = f;
563 move |(k, v)| {
564 let out = orig((Clone::clone(&k), v));
565 (k, out)
566 }
567 })
568 .splice_fn1_ctx::<(K, V), (K, U)>(&OperatorContext::<L, B::UnderlyingBound>::new(
569 &self.location,
570 ))
571 .into();
572
573 KeyedSingleton::new(
574 self.location.clone(),
575 HydroNode::Map {
576 f: map_f,
577 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
578 metadata: self.location.new_node_metadata(KeyedSingleton::<
579 K,
580 U,
581 L,
582 B::EraseMonotonic,
583 >::collection_kind()),
584 },
585 )
586 }
587
588 /// Gets the number of keys in the keyed singleton.
589 ///
590 /// The output singleton will be unbounded if the input is [`Unbounded`] or [`BoundedValue`],
591 /// since keys may be added / removed over time. When the set of keys changes, the count will
592 /// be asynchronously updated.
593 ///
594 /// # Example
595 /// ```rust
596 /// # #[cfg(feature = "deploy")] {
597 /// # use hydro_lang::prelude::*;
598 /// # use futures::StreamExt;
599 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
600 /// # let tick = process.tick();
601 /// let keyed_singleton = // { 1: "a", 2: "b", 3: "c" }
602 /// # process
603 /// # .source_iter(q!(vec![(1, "a"), (2, "b"), (3, "c")]))
604 /// # .into_keyed()
605 /// # .batch(&tick, nondet!(/** test */))
606 /// # .first();
607 /// keyed_singleton.key_count()
608 /// # .all_ticks()
609 /// # }, |mut stream| async move {
610 /// // 3
611 /// # assert_eq!(stream.next().await.unwrap(), 3);
612 /// # }));
613 /// # }
614 /// ```
615 pub fn key_count(self) -> Singleton<usize, L, B::UnderlyingBound> {
616 if B::ValueBound::BOUNDED {
617 let me: KeyedSingleton<K, V, L, B::WithBoundedValue> = KeyedSingleton {
618 location: self.location.clone(),
619 flow_state: self.flow_state.clone(),
620 ir_node: super::tracked_ir_node(
621 &self.flow_state,
622 self.ir_node.replace(HydroNode::Placeholder),
623 ),
624 _phantom: PhantomData,
625 };
626
627 me.entries().count().ignore_monotonic()
628 } else if L::is_top_level()
629 && let Some(tick) = self.location.try_tick()
630 && (B::bound_kind() == KeyedSingletonBoundKind::Unbounded
631 || B::bound_kind() == KeyedSingletonBoundKind::MonotonicKeys
632 || B::bound_kind() == KeyedSingletonBoundKind::MonotonicValue)
633 {
634 let location = self.location.clone();
635 let ir_node = self.ir_node.replace(HydroNode::Placeholder);
636 let me: KeyedSingleton<K, V, L, MonotonicKeys> =
637 KeyedSingleton::new(location.clone(), ir_node);
638
639 let out =
640 key_count_inside_tick(me.snapshot(&tick, nondet!(/** eventually stabilizes */)))
641 .latest();
642 Singleton::new(location, out.ir_node.replace(HydroNode::Placeholder))
643 } else {
644 panic!("BoundedValue or Unbounded KeyedSingleton inside a tick, not supported");
645 }
646 }
647
648 /// Converts this keyed singleton into a [`Singleton`] containing a `HashMap` from keys to values.
649 ///
650 /// As the values for each key are updated asynchronously, the `HashMap` will be updated
651 /// asynchronously as well.
652 ///
653 /// # Example
654 /// ```rust
655 /// # #[cfg(feature = "deploy")] {
656 /// # use hydro_lang::prelude::*;
657 /// # use futures::StreamExt;
658 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
659 /// let keyed_singleton = // { 1: "a", 2: "b", 3: "c" }
660 /// # process
661 /// # .source_iter(q!(vec![(1, "a".to_owned()), (2, "b".to_owned()), (3, "c".to_owned())]))
662 /// # .into_keyed()
663 /// # .batch(&process.tick(), nondet!(/** test */))
664 /// # .first();
665 /// keyed_singleton.into_singleton()
666 /// # .all_ticks()
667 /// # }, |mut stream| async move {
668 /// // { 1: "a", 2: "b", 3: "c" }
669 /// # assert_eq!(stream.next().await.unwrap(), vec![(1, "a".to_owned()), (2, "b".to_owned()), (3, "c".to_owned())].into_iter().collect());
670 /// # }));
671 /// # }
672 /// ```
673 pub fn into_singleton(self) -> Singleton<HashMap<K, V>, L, B::UnderlyingBound>
674 where
675 K: Eq + Hash,
676 {
677 if B::ValueBound::BOUNDED {
678 let me: KeyedSingleton<K, V, L, B::WithBoundedValue> = KeyedSingleton {
679 location: self.location.clone(),
680 flow_state: self.flow_state.clone(),
681 ir_node: super::tracked_ir_node(
682 &self.flow_state,
683 self.ir_node.replace(HydroNode::Placeholder),
684 ),
685 _phantom: PhantomData,
686 };
687
688 me.entries()
689 .assume_ordering_trusted(nondet!(
690 /// There is only one element associated with each key. The closure technically
691 /// isn't commutative in the case where both passed entries have the same key
692 /// but different values.
693 ///
694 /// In the future, we may want to have an `assume!(...)` statement in the UDF that
695 /// the key is never already present in the map.
696 ))
697 .fold(
698 q!(|| HashMap::new()),
699 q!(|map, (k, v)| {
700 // TODO(shadaj): make this commutative but really-debug-assert that there is no key overlap
701 map.insert(k, v);
702 }),
703 )
704 } else if L::is_top_level()
705 && let Some(tick) = self.location.try_tick()
706 && (B::bound_kind() == KeyedSingletonBoundKind::Unbounded
707 || B::bound_kind() == KeyedSingletonBoundKind::MonotonicKeys
708 || B::bound_kind() == KeyedSingletonBoundKind::MonotonicValue)
709 {
710 let location = self.location.clone();
711 let ir_node = self.ir_node.replace(HydroNode::Placeholder);
712 let me: KeyedSingleton<K, V, L, MonotonicKeys> =
713 KeyedSingleton::new(location.clone(), ir_node);
714
715 let out = into_singleton_inside_tick(
716 me.snapshot(&tick, nondet!(/** eventually stabilizes */)),
717 )
718 .latest();
719 Singleton::new(location, out.ir_node.replace(HydroNode::Placeholder))
720 } else {
721 panic!("BoundedValue or Unbounded KeyedSingleton inside a tick, not supported");
722 }
723 }
724
725 /// An operator which allows you to "name" a `HydroNode`.
726 /// This is only used for testing, to correlate certain `HydroNode`s with IDs.
727 pub fn ir_node_named(self, name: &str) -> KeyedSingleton<K, V, L, B> {
728 {
729 let mut node = self.ir_node.borrow_mut();
730 let metadata = node.metadata_mut();
731 metadata.tag = Some(name.to_owned());
732 }
733 self
734 }
735
736 /// Strengthens the boundedness guarantee to `Bounded`, given that `B: IsBounded`, which
737 /// implies that `B == Bounded`.
738 pub fn make_bounded(self) -> KeyedSingleton<K, V, L, Bounded>
739 where
740 B: IsBounded,
741 {
742 KeyedSingleton::new(
743 self.location.clone(),
744 self.ir_node.replace(HydroNode::Placeholder),
745 )
746 }
747
748 /// Gets the value associated with a specific key from the keyed singleton.
749 /// Returns `None` if the key is `None` or there is no associated value.
750 ///
751 /// # Example
752 /// ```rust
753 /// # #[cfg(feature = "deploy")] {
754 /// # use hydro_lang::prelude::*;
755 /// # use futures::StreamExt;
756 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
757 /// let tick = process.tick();
758 /// let keyed_data = process
759 /// .source_iter(q!(vec![(1, 2), (2, 3)]))
760 /// .into_keyed()
761 /// .batch(&tick, nondet!(/** test */))
762 /// .first();
763 /// let key = tick.singleton(q!(1));
764 /// keyed_data.get(key).all_ticks()
765 /// # }, |mut stream| async move {
766 /// // 2
767 /// # assert_eq!(stream.next().await.unwrap(), 2);
768 /// # }));
769 /// # }
770 /// ```
771 pub fn get(self, key: impl Into<Optional<K, L, Bounded>>) -> Optional<V, L, Bounded>
772 where
773 B: IsBounded,
774 K: Hash + Eq + Clone,
775 V: Clone,
776 {
777 self.make_bounded()
778 .into_keyed_stream()
779 .get(key)
780 .cast_at_most_one_element()
781 }
782
783 /// Emit a keyed stream containing keys shared between the keyed singleton and the
784 /// keyed stream, where each value in the output keyed stream is a tuple of
785 /// (the keyed singleton's value, the keyed stream's value).
786 ///
787 /// # Example
788 /// ```rust
789 /// # #[cfg(feature = "deploy")] {
790 /// # use hydro_lang::prelude::*;
791 /// # use futures::StreamExt;
792 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
793 /// let tick = process.tick();
794 /// let keyed_data = process
795 /// .source_iter(q!(vec![(1, 10), (2, 20)]))
796 /// .into_keyed()
797 /// .batch(&tick, nondet!(/** test */))
798 /// .first();
799 /// let other_data = process
800 /// .source_iter(q!(vec![(1, 100), (2, 200), (1, 101)]))
801 /// .into_keyed()
802 /// .batch(&tick, nondet!(/** test */));
803 /// keyed_data.join_keyed_stream(other_data).entries().all_ticks()
804 /// # }, |mut stream| async move {
805 /// // { 1: [(10, 100), (10, 101)], 2: [(20, 200)] } in any order
806 /// # let mut results = vec![];
807 /// # for _ in 0..3 {
808 /// # results.push(stream.next().await.unwrap());
809 /// # }
810 /// # results.sort();
811 /// # assert_eq!(results, vec![(1, (10, 100)), (1, (10, 101)), (2, (20, 200))]);
812 /// # }));
813 /// # }
814 /// ```
815 pub fn join_keyed_stream<O2: Ordering, R2: Retries, V2, B2: Boundedness>(
816 self,
817 other: KeyedStream<K, V2, L, B2, O2, R2>,
818 ) -> KeyedStream<K, (V, V2), L, B2, O2, R2>
819 where
820 B: IsBounded,
821 K: Eq + Hash + Clone,
822 V: Clone,
823 V2: Clone,
824 {
825 // TODO(shadaj): if DFIR guarantees that joining unbounded keyed stream x bounded keyed stream
826 // always produces deterministic order per key (nested loop join), this could just use
827 // `join_keyed_stream` without constructing IRs manually
828 KeyedStream::new(
829 self.location.clone(),
830 HydroNode::Join {
831 left: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
832 right: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
833 metadata: self
834 .location
835 .new_node_metadata(KeyedStream::<K, (V, V2), L, B2, O2, R2>::collection_kind()),
836 },
837 )
838 }
839
840 /// Emit a keyed singleton containing all keys shared between two keyed singletons,
841 /// where each value in the output keyed singleton is a tuple of
842 /// (self.value, other.value).
843 ///
844 /// # Example
845 /// ```rust
846 /// # #[cfg(feature = "deploy")] {
847 /// # use hydro_lang::prelude::*;
848 /// # use futures::StreamExt;
849 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
850 /// # let tick = process.tick();
851 /// let requests = // { 1: 10, 2: 20, 3: 30 }
852 /// # process
853 /// # .source_iter(q!(vec![(1, 10), (2, 20), (3, 30)]))
854 /// # .into_keyed()
855 /// # .batch(&tick, nondet!(/** test */))
856 /// # .first();
857 /// let other = // { 1: 100, 2: 200, 4: 400 }
858 /// # process
859 /// # .source_iter(q!(vec![(1, 100), (2, 200), (4, 400)]))
860 /// # .into_keyed()
861 /// # .batch(&tick, nondet!(/** test */))
862 /// # .first();
863 /// requests.join_keyed_singleton(other)
864 /// # .entries().all_ticks()
865 /// # }, |mut stream| async move {
866 /// // { 1: (10, 100), 2: (20, 200) }
867 /// # let mut results = vec![];
868 /// # for _ in 0..2 {
869 /// # results.push(stream.next().await.unwrap());
870 /// # }
871 /// # results.sort();
872 /// # assert_eq!(results, vec![(1, (10, 100)), (2, (20, 200))]);
873 /// # }));
874 /// # }
875 /// ```
876 pub fn join_keyed_singleton<V2: Clone>(
877 self,
878 other: KeyedSingleton<K, V2, L, Bounded>,
879 ) -> KeyedSingleton<K, (V, V2), L, Bounded>
880 where
881 B: IsBounded,
882 K: Eq + Hash + Clone,
883 V: Clone,
884 {
885 let result_stream = self
886 .make_bounded()
887 .entries()
888 .join(other.entries())
889 .into_keyed();
890
891 // The cast is guaranteed to succeed, since each key (in both `self` and `other`) has at most one value.
892 result_stream.cast_at_most_one_entry_per_key()
893 }
894
895 /// For each value in `self`, find the matching key in `lookup`.
896 /// The output is a keyed singleton with the key from `self`, and a value
897 /// that is a tuple of (`self`'s value, Option<`lookup`'s value>).
898 /// If the key is not present in `lookup`, the option will be [`None`].
899 ///
900 /// # Example
901 /// ```rust
902 /// # #[cfg(feature = "deploy")] {
903 /// # use hydro_lang::prelude::*;
904 /// # use futures::StreamExt;
905 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
906 /// # let tick = process.tick();
907 /// let requests = // { 1: 10, 2: 20 }
908 /// # process
909 /// # .source_iter(q!(vec![(1, 10), (2, 20)]))
910 /// # .into_keyed()
911 /// # .batch(&tick, nondet!(/** test */))
912 /// # .first();
913 /// let other_data = // { 10: 100, 11: 110 }
914 /// # process
915 /// # .source_iter(q!(vec![(10, 100), (11, 110)]))
916 /// # .into_keyed()
917 /// # .batch(&tick, nondet!(/** test */))
918 /// # .first();
919 /// requests.lookup_keyed_singleton(other_data)
920 /// # .entries().all_ticks()
921 /// # }, |mut stream| async move {
922 /// // { 1: (10, Some(100)), 2: (20, None) }
923 /// # let mut results = vec![];
924 /// # for _ in 0..2 {
925 /// # results.push(stream.next().await.unwrap());
926 /// # }
927 /// # results.sort();
928 /// # assert_eq!(results, vec![(1, (10, Some(100))), (2, (20, None))]);
929 /// # }));
930 /// # }
931 /// ```
932 pub fn lookup_keyed_singleton<V2>(
933 self,
934 lookup: KeyedSingleton<V, V2, L, Bounded>,
935 ) -> KeyedSingleton<K, (V, Option<V2>), L, Bounded>
936 where
937 B: IsBounded,
938 K: Eq + Hash + Clone,
939 V: Eq + Hash + Clone,
940 V2: Clone,
941 {
942 let result_stream = self
943 .make_bounded()
944 .into_keyed_stream()
945 .lookup_keyed_stream(lookup.into_keyed_stream());
946
947 // The cast is guaranteed to succeed since both lookup and self contain at most 1 value per key
948 result_stream.cast_at_most_one_entry_per_key()
949 }
950
951 /// For each value in `self`, find the matching key in `lookup`.
952 /// The output is a keyed stream with the key from `self`, and a value
953 /// that is a tuple of (`self`'s value, Option<`lookup`'s value>).
954 /// If the key is not present in `lookup`, the option will be [`None`].
955 ///
956 /// # Example
957 /// ```rust
958 /// # #[cfg(feature = "deploy")] {
959 /// # use hydro_lang::prelude::*;
960 /// # use futures::StreamExt;
961 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
962 /// # let tick = process.tick();
963 /// let requests = // { 1: 10, 2: 20 }
964 /// # process
965 /// # .source_iter(q!(vec![(1, 10), (2, 20)]))
966 /// # .into_keyed()
967 /// # .batch(&tick, nondet!(/** test */))
968 /// # .first();
969 /// let other_data = // { 10: 100, 10: 110 }
970 /// # process
971 /// # .source_iter(q!(vec![(10, 100), (10, 110)]))
972 /// # .into_keyed()
973 /// # .batch(&tick, nondet!(/** test */));
974 /// requests.lookup_keyed_stream(other_data)
975 /// # .entries().all_ticks()
976 /// # }, |mut stream| async move {
977 /// // { 1: [(10, Some(100)), (10, Some(110))], 2: (20, None) }
978 /// # let mut results = vec![];
979 /// # for _ in 0..3 {
980 /// # results.push(stream.next().await.unwrap());
981 /// # }
982 /// # results.sort();
983 /// # assert_eq!(results, vec![(1, (10, Some(100))), (1, (10, Some(110))), (2, (20, None))]);
984 /// # }));
985 /// # }
986 /// ```
987 pub fn lookup_keyed_stream<V2, O: Ordering, R: Retries>(
988 self,
989 lookup: KeyedStream<V, V2, L, Bounded, O, R>,
990 ) -> KeyedStream<K, (V, Option<V2>), L, Bounded, NoOrder, R>
991 where
992 B: IsBounded,
993 K: Eq + Hash + Clone,
994 V: Eq + Hash + Clone,
995 V2: Clone,
996 {
997 self.make_bounded()
998 .entries()
999 .weaken_retries::<R>() // TODO: Once weaken_retries() is implemented for KeyedSingleton, remove entries() and into_keyed()
1000 .into_keyed()
1001 .lookup_keyed_stream(lookup)
1002 }
1003
1004 /// For each key present in both `self` and `thresholds`, emits a [`KeyedStream`] event the first
1005 /// time that key's value becomes greater than or equal to the corresponding threshold value.
1006 /// The emitted value for each key is the threshold value itself.
1007 ///
1008 /// This requires the keyed singleton to have monotonic values ([`MonotonicValue`] or [`Bounded`]),
1009 /// because otherwise the threshold detection would be non-deterministic.
1010 ///
1011 /// The `thresholds` parameter is a [`BoundedValue`] keyed singleton mapping each key to its
1012 /// threshold. Thresholds may arrive asynchronously (new keys appear over time), but once set
1013 /// for a key, the threshold value is fixed. Late-arriving thresholds are checked against the
1014 /// current snapshot value immediately.
1015 ///
1016 /// # Example
1017 /// ```rust,ignore
1018 /// use hydro_lang::prelude::*;
1019 ///
1020 /// // Given a monotonically increasing keyed singleton (e.g. from fold with monotone proof)
1021 /// let counts: KeyedSingleton<u32, usize, _, MonotonicValue> = events.into_keyed()
1022 /// .fold(q!(|| 0), q!(|acc, _| *acc += 1, monotone = manual_proof!(/** +1 is monotone */)));
1023 ///
1024 /// // BoundedValue keyed singleton of thresholds (from .first())
1025 /// let thresholds = threshold_source.into_keyed().first();
1026 ///
1027 /// // Emits (key, threshold_value) the first time each key's value >= threshold
1028 /// let crossed = counts.threshold_greater_or_equal(thresholds);
1029 /// ```
1030 pub fn threshold_greater_or_equal(
1031 self,
1032 thresholds: KeyedSingleton<K, V, L, BoundedValue>,
1033 ) -> KeyedStream<K, V, L, B::UnderlyingBound, NoOrder, ExactlyOnce>
1034 where
1035 K: Clone + Eq + Hash,
1036 V: Clone + PartialOrd,
1037 B: IsKeyedMonotonic,
1038 {
1039 let self_location = self.location.clone();
1040 match B::bound_kind() {
1041 KeyedSingletonBoundKind::Bounded => {
1042 // Bounded case: self is already fixed, just join and filter
1043 let me: KeyedSingleton<K, V, L, Bounded> = KeyedSingleton::new(
1044 self.location.clone(),
1045 self.ir_node.replace(HydroNode::Placeholder),
1046 );
1047 let result = me
1048 .entries()
1049 .join(thresholds.entries())
1050 .filter_map(q!(|(k, (val, thresh))| {
1051 if val >= thresh {
1052 Some((k, thresh))
1053 } else {
1054 None
1055 }
1056 }))
1057 .into_keyed();
1058 KeyedStream::new(
1059 result.location.clone(),
1060 result.ir_node.replace(HydroNode::Placeholder),
1061 )
1062 }
1063 KeyedSingletonBoundKind::MonotonicValue => {
1064 let me: KeyedSingleton<K, V, L, MonotonicValue> = KeyedSingleton::new(
1065 self.location.clone(),
1066 self.ir_node.replace(HydroNode::Placeholder),
1067 );
1068
1069 let result = sliced! {
1070 let snapshot = use::snapshot(me, nondet!(/** thresholds are deterministic */));
1071 let thresh_snapshot =
1072 use::batch(thresholds, nondet!(/** thresholds are deterministic */));
1073 let mut already_crossed =
1074 use::state_null::<Stream<K, Tick<_>, Bounded, NoOrder>>();
1075
1076 let joined = thresh_snapshot.entries().join(snapshot.entries());
1077 let passed = joined
1078 .filter(q!(|(_, (thresh, val))| *val >= *thresh))
1079 .map(q!(|(k, (thresh, _))| (k, thresh)));
1080
1081 let newly_crossed = passed.anti_join(already_crossed.clone());
1082 already_crossed =
1083 already_crossed.chain(newly_crossed.clone().map(q!(|(k, _)| k)));
1084
1085 newly_crossed.into_keyed()
1086 };
1087
1088 KeyedStream::new(
1089 self_location,
1090 result.ir_node.replace(HydroNode::Placeholder),
1091 )
1092 }
1093 KeyedSingletonBoundKind::BoundedValue => {
1094 let me: KeyedSingleton<K, V, L, BoundedValue> = KeyedSingleton::new(
1095 self.location.clone(),
1096 self.ir_node.replace(HydroNode::Placeholder),
1097 );
1098
1099 let result = sliced! {
1100 let snapshot = use::batch(me, nondet!(/** thresholds are deterministic */));
1101 let thresh_snapshot =
1102 use::batch(thresholds, nondet!(/** thresholds are deterministic */));
1103 let mut already_crossed =
1104 use::state_null::<Stream<K, Tick<_>, Bounded, NoOrder>>();
1105
1106 let joined = thresh_snapshot.entries().join(snapshot.entries());
1107 let passed = joined
1108 .filter(q!(|(_, (thresh, val))| *val >= *thresh))
1109 .map(q!(|(k, (thresh, _))| (k, thresh)));
1110
1111 let newly_crossed = passed.anti_join(already_crossed.clone());
1112 already_crossed =
1113 already_crossed.chain(newly_crossed.clone().map(q!(|(k, _)| k)));
1114
1115 newly_crossed.into_keyed()
1116 };
1117
1118 KeyedStream::new(
1119 self_location,
1120 result.ir_node.replace(HydroNode::Placeholder),
1121 )
1122 }
1123 _ => {
1124 unreachable!(
1125 "IsKeyedMonotonic is only implemented for Bounded, BoundedValue, and MonotonicValue"
1126 )
1127 }
1128 }
1129 }
1130
1131 /// Like [`Self::threshold_greater_or_equal`], but uses a single [`Singleton`] threshold
1132 /// shared across all keys. Emits a `(K, V)` event for each key the first time that key's
1133 /// value becomes >= the threshold. The emitted value is the threshold itself.
1134 ///
1135 /// Because the threshold is a [`Bounded`] singleton, it is a compile-time constant and
1136 /// does not carry ongoing memory cost.
1137 ///
1138 /// # Example
1139 /// ```rust
1140 /// # #[cfg(feature = "deploy")] {
1141 /// # use hydro_lang::prelude::*;
1142 /// # use futures::StreamExt;
1143 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1144 /// // A keyed singleton of per-key values (in practice often a monotone counter): { 1: 6, 2: 4 }
1145 /// let counts = process
1146 /// .source_iter(q!(vec![(1, 6), (2, 4)]))
1147 /// .into_keyed()
1148 /// .first();
1149 ///
1150 /// // A single threshold value shared across all keys
1151 /// let threshold = process.singleton(q!(5));
1152 ///
1153 /// // Emits (key, threshold) the first time each key's value >= threshold
1154 /// counts.threshold_greater_or_equal_uniform(threshold)
1155 /// # .entries()
1156 /// # }, |mut stream| async move {
1157 /// // { 1: 5 } -- key 1's value 6 >= 5, but key 2's value 4 < 5
1158 /// # assert_eq!(stream.next().await.unwrap(), (1, 5));
1159 /// # }));
1160 /// # }
1161 /// ```
1162 pub fn threshold_greater_or_equal_uniform(
1163 self,
1164 threshold: Singleton<V, L, Bounded>,
1165 ) -> KeyedStream<K, V, L, B::UnderlyingBound, NoOrder, ExactlyOnce>
1166 where
1167 K: Clone + Eq + Hash,
1168 V: Clone + PartialOrd,
1169 B: IsKeyedMonotonic,
1170 {
1171 let self_location = self.location.clone();
1172 match B::bound_kind() {
1173 KeyedSingletonBoundKind::Bounded => {
1174 let me: KeyedSingleton<K, V, L, Bounded> = KeyedSingleton::new(
1175 self.location.clone(),
1176 self.ir_node.replace(HydroNode::Placeholder),
1177 );
1178 let result = me
1179 .entries()
1180 .cross_singleton(threshold)
1181 .filter_map(q!(|((k, val), thresh)| {
1182 if val >= thresh {
1183 Some((k, thresh))
1184 } else {
1185 None
1186 }
1187 }))
1188 .into_keyed();
1189 KeyedStream::new(
1190 result.location.clone(),
1191 result.ir_node.replace(HydroNode::Placeholder),
1192 )
1193 }
1194 KeyedSingletonBoundKind::MonotonicValue => {
1195 let me: KeyedSingleton<K, V, L, MonotonicValue> = KeyedSingleton::new(
1196 self.location.clone(),
1197 self.ir_node.replace(HydroNode::Placeholder),
1198 );
1199
1200 let result = sliced! {
1201 let snapshot = use::snapshot(me, nondet!(/** thresholds are deterministic */));
1202 let mut already_crossed =
1203 use::state_null::<Stream<K, Tick<_>, Bounded, NoOrder>>();
1204
1205 let tick = snapshot.location().clone();
1206 let thresh_in_tick = threshold.clone_into_tick(&tick);
1207
1208 let crossing = snapshot
1209 .entries()
1210 .cross_singleton(thresh_in_tick)
1211 .filter_map(q!(|((k, val), thresh)| {
1212 if val >= thresh {
1213 Some((k, thresh))
1214 } else {
1215 None
1216 }
1217 }));
1218
1219 let newly_crossed = crossing.anti_join(already_crossed.clone());
1220 already_crossed =
1221 already_crossed.chain(newly_crossed.clone().map(q!(|(k, _)| k)));
1222
1223 newly_crossed.into_keyed()
1224 };
1225
1226 KeyedStream::new(
1227 self_location,
1228 result.ir_node.replace(HydroNode::Placeholder),
1229 )
1230 }
1231 KeyedSingletonBoundKind::BoundedValue => {
1232 let me: KeyedSingleton<K, V, L, BoundedValue> = KeyedSingleton::new(
1233 self.location.clone(),
1234 self.ir_node.replace(HydroNode::Placeholder),
1235 );
1236
1237 let result = sliced! {
1238 let snapshot = use::batch(me, nondet!(/** thresholds are deterministic */));
1239 let mut already_crossed =
1240 use::state_null::<Stream<K, Tick<_>, Bounded, NoOrder>>();
1241
1242 let tick = snapshot.location().clone();
1243 let thresh_in_tick = threshold.clone_into_tick(&tick);
1244
1245 let crossing = snapshot
1246 .entries()
1247 .cross_singleton(thresh_in_tick)
1248 .filter_map(q!(|((k, val), thresh)| {
1249 if val >= thresh {
1250 Some((k, thresh))
1251 } else {
1252 None
1253 }
1254 }));
1255
1256 let newly_crossed = crossing.anti_join(already_crossed.clone());
1257 already_crossed =
1258 already_crossed.chain(newly_crossed.clone().map(q!(|(k, _)| k)));
1259
1260 newly_crossed.into_keyed()
1261 };
1262
1263 KeyedStream::new(
1264 self_location,
1265 result.ir_node.replace(HydroNode::Placeholder),
1266 )
1267 }
1268 _ => {
1269 unreachable!(
1270 "IsKeyedMonotonic is only implemented for Bounded, BoundedValue, and MonotonicValue"
1271 )
1272 }
1273 }
1274 }
1275}
1276
1277impl<'a, K, V, L: Location<'a>, B: KeyedSingletonBound<ValueBound = Bounded>>
1278 KeyedSingleton<K, V, L, B>
1279{
1280 /// Flattens the keyed singleton into an unordered stream of key-value pairs.
1281 ///
1282 /// The value for each key must be bounded, otherwise the resulting stream elements would be
1283 /// non-deterministic. As new entries are added to the keyed singleton, they will be streamed
1284 /// into the output.
1285 ///
1286 /// # Example
1287 /// ```rust
1288 /// # #[cfg(feature = "deploy")] {
1289 /// # use hydro_lang::prelude::*;
1290 /// # use futures::StreamExt;
1291 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1292 /// let keyed_singleton = // { 1: 2, 2: 4 }
1293 /// # process
1294 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1295 /// # .into_keyed()
1296 /// # .first();
1297 /// keyed_singleton.entries()
1298 /// # }, |mut stream| async move {
1299 /// // (1, 2), (2, 4) in any order
1300 /// # let mut results = Vec::new();
1301 /// # for _ in 0..2 {
1302 /// # results.push(stream.next().await.unwrap());
1303 /// # }
1304 /// # results.sort();
1305 /// # assert_eq!(results, vec![(1, 2), (2, 4)]);
1306 /// # }));
1307 /// # }
1308 /// ```
1309 pub fn entries(self) -> Stream<(K, V), L, B::UnderlyingBound, NoOrder, ExactlyOnce> {
1310 self.into_keyed_stream().entries()
1311 }
1312
1313 /// Flattens the keyed singleton into an unordered stream of just the values.
1314 ///
1315 /// The value for each key must be bounded, otherwise the resulting stream elements would be
1316 /// non-deterministic. As new entries are added to the keyed singleton, they will be streamed
1317 /// into the output.
1318 ///
1319 /// # Example
1320 /// ```rust
1321 /// # #[cfg(feature = "deploy")] {
1322 /// # use hydro_lang::prelude::*;
1323 /// # use futures::StreamExt;
1324 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1325 /// let keyed_singleton = // { 1: 2, 2: 4 }
1326 /// # process
1327 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1328 /// # .into_keyed()
1329 /// # .first();
1330 /// keyed_singleton.values()
1331 /// # }, |mut stream| async move {
1332 /// // 2, 4 in any order
1333 /// # let mut results = Vec::new();
1334 /// # for _ in 0..2 {
1335 /// # results.push(stream.next().await.unwrap());
1336 /// # }
1337 /// # results.sort();
1338 /// # assert_eq!(results, vec![2, 4]);
1339 /// # }));
1340 /// # }
1341 /// ```
1342 pub fn values(self) -> Stream<V, L, B::UnderlyingBound, NoOrder, ExactlyOnce> {
1343 let map_f = q!(|(_, v)| v)
1344 .splice_fn1_ctx::<(K, V), V>(&OperatorContext::<L, B::UnderlyingBound>::new(
1345 &self.location,
1346 ))
1347 .into();
1348
1349 Stream::new(
1350 self.location.clone(),
1351 HydroNode::Map {
1352 f: map_f,
1353 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1354 metadata: self.location.new_node_metadata(Stream::<
1355 V,
1356 L,
1357 B::UnderlyingBound,
1358 NoOrder,
1359 ExactlyOnce,
1360 >::collection_kind()),
1361 },
1362 )
1363 }
1364
1365 /// Flattens the keyed singleton into an unordered stream of just the keys.
1366 ///
1367 /// The value for each key must be bounded, otherwise the removal of keys would result in
1368 /// non-determinism. As new entries are added to the keyed singleton, they will be streamed
1369 /// into the output.
1370 ///
1371 /// # Example
1372 /// ```rust
1373 /// # #[cfg(feature = "deploy")] {
1374 /// # use hydro_lang::prelude::*;
1375 /// # use futures::StreamExt;
1376 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1377 /// let keyed_singleton = // { 1: 2, 2: 4 }
1378 /// # process
1379 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1380 /// # .into_keyed()
1381 /// # .first();
1382 /// keyed_singleton.keys()
1383 /// # }, |mut stream| async move {
1384 /// // 1, 2 in any order
1385 /// # let mut results = Vec::new();
1386 /// # for _ in 0..2 {
1387 /// # results.push(stream.next().await.unwrap());
1388 /// # }
1389 /// # results.sort();
1390 /// # assert_eq!(results, vec![1, 2]);
1391 /// # }));
1392 /// # }
1393 /// ```
1394 pub fn keys(self) -> Stream<K, L, B::UnderlyingBound, NoOrder, ExactlyOnce> {
1395 self.entries().map(q!(|(k, _)| k))
1396 }
1397
1398 /// Given a bounded stream of keys `K`, returns a new keyed singleton containing only the
1399 /// entries whose keys are not in the provided stream.
1400 ///
1401 /// # Example
1402 /// ```rust
1403 /// # #[cfg(feature = "deploy")] {
1404 /// # use hydro_lang::prelude::*;
1405 /// # use futures::StreamExt;
1406 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1407 /// let tick = process.tick();
1408 /// let keyed_singleton = // { 1: 2, 2: 4 }
1409 /// # process
1410 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1411 /// # .into_keyed()
1412 /// # .first()
1413 /// # .batch(&tick, nondet!(/** test */));
1414 /// let keys_to_remove = process
1415 /// .source_iter(q!(vec![1]))
1416 /// .batch(&tick, nondet!(/** test */));
1417 /// keyed_singleton.filter_key_not_in(keys_to_remove)
1418 /// # .entries().all_ticks()
1419 /// # }, |mut stream| async move {
1420 /// // { 2: 4 }
1421 /// # for w in vec![(2, 4)] {
1422 /// # assert_eq!(stream.next().await.unwrap(), w);
1423 /// # }
1424 /// # }));
1425 /// # }
1426 /// ```
1427 pub fn filter_key_not_in<O2: Ordering, R2: Retries>(
1428 self,
1429 other: Stream<K, L, Bounded, O2, R2>,
1430 ) -> Self
1431 where
1432 K: Hash + Eq,
1433 {
1434 check_matching_location(&self.location, &other.location);
1435
1436 KeyedSingleton::new(
1437 self.location.clone(),
1438 HydroNode::AntiJoin {
1439 pos: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1440 neg: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
1441 metadata: self.location.new_node_metadata(Self::collection_kind()),
1442 },
1443 )
1444 }
1445
1446 /// An operator which allows you to "inspect" each value of a keyed singleton without
1447 /// modifying it. The closure `f` is called on a reference to each value. This is
1448 /// mainly useful for debugging, and should not be used to generate side-effects.
1449 ///
1450 /// # Example
1451 /// ```rust
1452 /// # #[cfg(feature = "deploy")] {
1453 /// # use hydro_lang::prelude::*;
1454 /// # use futures::StreamExt;
1455 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1456 /// let keyed_singleton = // { 1: 2, 2: 4 }
1457 /// # process
1458 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1459 /// # .into_keyed()
1460 /// # .first();
1461 /// keyed_singleton
1462 /// .inspect(q!(|v| println!("{}", v)))
1463 /// # .entries()
1464 /// # }, |mut stream| async move {
1465 /// // { 1: 2, 2: 4 }
1466 /// # for w in vec![(1, 2), (2, 4)] {
1467 /// # assert_eq!(stream.next().await.unwrap(), w);
1468 /// # }
1469 /// # }));
1470 /// # }
1471 /// ```
1472 pub fn inspect<F>(
1473 self,
1474 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>> + Copy,
1475 ) -> Self
1476 where
1477 F: Fn(&V) + 'a,
1478 {
1479 let f: ManualExpr<F, _> =
1480 ManualExpr::new(move |ctx: &OperatorContext<L, B::UnderlyingBound>| {
1481 f.splice_fn1_borrow_ctx(ctx)
1482 });
1483 let inspect_f = q!({
1484 let orig = f;
1485 move |t: &(_, _)| orig(&t.1)
1486 })
1487 .splice_fn1_borrow_ctx::<(K, V), ()>(&OperatorContext::<L, B::UnderlyingBound>::new(
1488 &self.location,
1489 ))
1490 .into();
1491
1492 KeyedSingleton::new(
1493 self.location.clone(),
1494 HydroNode::Inspect {
1495 f: inspect_f,
1496 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1497 metadata: self.location.new_node_metadata(Self::collection_kind()),
1498 },
1499 )
1500 }
1501
1502 /// An operator which allows you to "inspect" each entry of a keyed singleton without
1503 /// modifying it. The closure `f` is called on a reference to each key-value pair. This is
1504 /// mainly useful for debugging, and should not be used to generate side-effects.
1505 ///
1506 /// # Example
1507 /// ```rust
1508 /// # #[cfg(feature = "deploy")] {
1509 /// # use hydro_lang::prelude::*;
1510 /// # use futures::StreamExt;
1511 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1512 /// let keyed_singleton = // { 1: 2, 2: 4 }
1513 /// # process
1514 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1515 /// # .into_keyed()
1516 /// # .first();
1517 /// keyed_singleton
1518 /// .inspect_with_key(q!(|(k, v)| println!("{}: {}", k, v)))
1519 /// # .entries()
1520 /// # }, |mut stream| async move {
1521 /// // { 1: 2, 2: 4 }
1522 /// # for w in vec![(1, 2), (2, 4)] {
1523 /// # assert_eq!(stream.next().await.unwrap(), w);
1524 /// # }
1525 /// # }));
1526 /// # }
1527 /// ```
1528 pub fn inspect_with_key<F>(
1529 self,
1530 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>>,
1531 ) -> Self
1532 where
1533 F: Fn(&(K, V)) + 'a,
1534 {
1535 let inspect_f = f
1536 .splice_fn1_borrow_ctx::<(K, V), ()>(&OperatorContext::<L, B::UnderlyingBound>::new(
1537 &self.location,
1538 ))
1539 .into();
1540
1541 KeyedSingleton::new(
1542 self.location.clone(),
1543 HydroNode::Inspect {
1544 f: inspect_f,
1545 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1546 metadata: self.location.new_node_metadata(Self::collection_kind()),
1547 },
1548 )
1549 }
1550
1551 /// Gets the key-value tuple with the largest key among all entries in this [`KeyedSingleton`].
1552 ///
1553 /// Because this method requires values to be bounded, the output [`Optional`] will only be
1554 /// asynchronously updated if a new key is added that is higher than the previous max key.
1555 ///
1556 /// # Example
1557 /// ```rust
1558 /// # #[cfg(feature = "deploy")] {
1559 /// # use hydro_lang::prelude::*;
1560 /// # use futures::StreamExt;
1561 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1562 /// let tick = process.tick();
1563 /// let keyed_singleton = // { 1: 123, 2: 456, 0: 789 }
1564 /// # Stream::<_, _>::from(process.source_iter(q!(vec![(1, 123), (2, 456), (0, 789)])))
1565 /// # .into_keyed()
1566 /// # .first();
1567 /// keyed_singleton.get_max_key()
1568 /// # .sample_eager(nondet!(/** test */))
1569 /// # }, |mut stream| async move {
1570 /// // (2, 456)
1571 /// # assert_eq!(stream.next().await.unwrap(), (2, 456));
1572 /// # }));
1573 /// # }
1574 /// ```
1575 pub fn get_max_key(self) -> Optional<(K, V), L, B::UnderlyingBound>
1576 where
1577 K: Ord,
1578 {
1579 self.entries()
1580 .assume_ordering_trusted(nondet!(
1581 /// There is only one element associated with each key, and the keys are totallly
1582 /// ordered so we will produce a deterministic value. The closure technically
1583 /// isn't commutative in the case where both passed entries have the same key
1584 /// but different values.
1585 ///
1586 /// In the future, we may want to have an `assume!(...)` statement in the UDF that
1587 /// the two inputs do not have the same key.
1588 ))
1589 .reduce(q!(
1590 move |curr, new| {
1591 if new.0 > curr.0 {
1592 *curr = new;
1593 }
1594 },
1595 idempotent = manual_proof!(/** repeated elements are ignored */)
1596 ))
1597 }
1598
1599 /// Converts this keyed singleton into a [`KeyedStream`] with each group having a single
1600 /// element, the value.
1601 ///
1602 /// This is the equivalent of [`Singleton::into_stream`] but keyed.
1603 ///
1604 /// # Example
1605 /// ```rust
1606 /// # #[cfg(feature = "deploy")] {
1607 /// # use hydro_lang::prelude::*;
1608 /// # use futures::StreamExt;
1609 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1610 /// let keyed_singleton = // { 1: 2, 2: 4 }
1611 /// # Stream::<_, _>::from(process.source_iter(q!(vec![(1, 2), (2, 4)])))
1612 /// # .into_keyed()
1613 /// # .first();
1614 /// keyed_singleton
1615 /// .clone()
1616 /// .into_keyed_stream()
1617 /// .merge_unordered(
1618 /// keyed_singleton.into_keyed_stream()
1619 /// )
1620 /// # .entries()
1621 /// # }, |mut stream| async move {
1622 /// /// // { 1: [2, 2], 2: [4, 4] }
1623 /// # for w in vec![(1, 2), (2, 4), (1, 2), (2, 4)] {
1624 /// # assert_eq!(stream.next().await.unwrap(), w);
1625 /// # }
1626 /// # }));
1627 /// # }
1628 /// ```
1629 pub fn into_keyed_stream(
1630 self,
1631 ) -> KeyedStream<K, V, L, B::UnderlyingBound, TotalOrder, ExactlyOnce> {
1632 KeyedStream::new(
1633 self.location.clone(),
1634 HydroNode::Cast {
1635 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1636 metadata: self.location.new_node_metadata(KeyedStream::<
1637 K,
1638 V,
1639 L,
1640 B::UnderlyingBound,
1641 TotalOrder,
1642 ExactlyOnce,
1643 >::collection_kind()),
1644 },
1645 )
1646 }
1647}
1648
1649impl<'a, K, V, L, B: KeyedSingletonBound> KeyedSingleton<K, V, L, B>
1650where
1651 L: Location<'a>,
1652 B: KeyedSingletonBound<ValueBound = Bounded>,
1653{
1654 /// Shifts this bounded-value keyed singleton into an atomic context, which guarantees that any downstream logic
1655 /// will all be executed synchronously before any outputs are yielded (in [`KeyedSingleton::end_atomic`]).
1656 ///
1657 /// This is useful to enforce local consistency constraints, such as ensuring that a write is
1658 /// processed before an acknowledgement is emitted.
1659 pub fn atomic(self) -> KeyedSingleton<K, V, Atomic<L>, B> {
1660 let id = self.location.flow_state().borrow_mut().next_clock_id();
1661 let out_location = Atomic {
1662 tick: Tick {
1663 id,
1664 l: self.location.clone(),
1665 },
1666 };
1667 KeyedSingleton::new(
1668 out_location.clone(),
1669 HydroNode::BeginAtomic {
1670 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1671 metadata: out_location
1672 .new_node_metadata(KeyedSingleton::<K, V, Atomic<L>, B>::collection_kind()),
1673 },
1674 )
1675 }
1676}
1677
1678impl<'a, K, V, L, B: KeyedSingletonBound> KeyedSingleton<K, V, Atomic<L>, B>
1679where
1680 L: Location<'a>,
1681{
1682 /// Yields the elements of this keyed singleton back into a top-level, asynchronous execution context.
1683 /// See [`KeyedSingleton::atomic`] for more details.
1684 pub fn end_atomic(self) -> KeyedSingleton<K, V, L, B> {
1685 KeyedSingleton::new(
1686 self.location.tick.l.clone(),
1687 HydroNode::EndAtomic {
1688 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1689 metadata: self
1690 .location
1691 .tick
1692 .l
1693 .new_node_metadata(KeyedSingleton::<K, V, L, B>::collection_kind()),
1694 },
1695 )
1696 }
1697}
1698
1699impl<'a, K, V, L: Location<'a>> KeyedSingleton<K, V, Tick<L>, Bounded> {
1700 /// Shifts the state in `self` to the **next tick**, so that the returned keyed singleton at
1701 /// tick `T` always has the entries of `self` at tick `T - 1`.
1702 ///
1703 /// At tick `0`, the output has no entries, since there is no previous tick.
1704 ///
1705 /// This operator enables stateful iterative processing with ticks, by sending data from one
1706 /// tick to the next. For example, you can use it to compare state across consecutive batches.
1707 ///
1708 /// # Example
1709 /// ```rust
1710 /// # #[cfg(feature = "deploy")] {
1711 /// # use hydro_lang::prelude::*;
1712 /// # use futures::StreamExt;
1713 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1714 /// let tick = process.tick();
1715 /// # // ticks are lazy by default, forces the second tick to run
1716 /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1717 /// # let batch_first_tick = process
1718 /// # .source_iter(q!(vec![(1, 2), (2, 3)]))
1719 /// # .batch(&tick, nondet!(/** test */))
1720 /// # .into_keyed();
1721 /// # let batch_second_tick = process
1722 /// # .source_iter(q!(vec![(2, 4), (3, 5)]))
1723 /// # .batch(&tick, nondet!(/** test */))
1724 /// # .into_keyed()
1725 /// # .defer_tick(); // appears on the second tick
1726 /// let input_batch = // first tick: { 1: 2, 2: 3 }, second tick: { 2: 4, 3: 5 }
1727 /// # batch_first_tick.chain(batch_second_tick).first();
1728 /// input_batch.clone().filter_key_not_in(
1729 /// input_batch.defer_tick().keys() // keys present in the previous tick
1730 /// )
1731 /// # .entries().all_ticks()
1732 /// # }, |mut stream| async move {
1733 /// // { 1: 2, 2: 3 } (first tick), { 3: 5 } (second tick)
1734 /// # for w in vec![(1, 2), (2, 3), (3, 5)] {
1735 /// # assert_eq!(stream.next().await.unwrap(), w);
1736 /// # }
1737 /// # }));
1738 /// # }
1739 /// ```
1740 pub fn defer_tick(self) -> KeyedSingleton<K, V, Tick<L>, Bounded> {
1741 KeyedSingleton::new(
1742 self.location.clone(),
1743 HydroNode::DeferTick {
1744 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1745 metadata: self
1746 .location
1747 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1748 },
1749 )
1750 }
1751}
1752
1753impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Unbounded>> KeyedSingleton<K, V, L, B>
1754where
1755 L: Location<'a>,
1756{
1757 /// Returns a keyed singleton with a snapshot of each key-value entry at a non-deterministic
1758 /// point in time.
1759 ///
1760 /// # Non-Determinism
1761 /// Because this picks a snapshot of each entry, which is continuously changing, each output has a
1762 /// non-deterministic set of entries since each snapshot can be at an arbitrary point in time.
1763 pub fn snapshot<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1764 self,
1765 tick: &Tick<L2>,
1766 _nondet: NonDet,
1767 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
1768 assert_eq!(Location::id(tick.outer()), Location::id(&self.location));
1769 KeyedSingleton::new(
1770 tick.drop_consistency(),
1771 HydroNode::Batch {
1772 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1773 metadata: tick
1774 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1775 },
1776 )
1777 }
1778}
1779
1780impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Unbounded>> KeyedSingleton<K, V, Atomic<L>, B>
1781where
1782 L: Location<'a>,
1783{
1784 /// Returns a keyed singleton with a snapshot of each key-value entry, consistent with the
1785 /// state of the keyed singleton being atomically processed.
1786 ///
1787 /// # Non-Determinism
1788 /// Because this picks a snapshot of each entry, which is continuously changing, each output has a
1789 /// non-deterministic set of entries since each snapshot can be at an arbitrary point in time.
1790 pub fn snapshot_atomic<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1791 self,
1792 tick: &Tick<L2>,
1793 _nondet: NonDet,
1794 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
1795 KeyedSingleton::new(
1796 tick.drop_consistency(),
1797 HydroNode::Batch {
1798 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1799 metadata: tick
1800 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1801 },
1802 )
1803 }
1804}
1805
1806impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Bounded>> KeyedSingleton<K, V, L, B>
1807where
1808 L: Location<'a>,
1809{
1810 /// Creates a keyed singleton containing only the key-value pairs where the value satisfies a predicate `f`.
1811 ///
1812 /// The closure `f` receives a reference `&V` to each value and returns a boolean. If the predicate
1813 /// returns `true`, the key-value pair is included in the output. If it returns `false`, the pair
1814 /// is filtered out.
1815 ///
1816 /// The closure `f` receives a reference `&V` rather than an owned value `V` because filtering does
1817 /// not modify or take ownership of the values. If you need to modify the values while filtering
1818 /// use [`KeyedSingleton::filter_map`] instead.
1819 ///
1820 /// # Example
1821 /// ```rust
1822 /// # #[cfg(feature = "deploy")] {
1823 /// # use hydro_lang::prelude::*;
1824 /// # use futures::StreamExt;
1825 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1826 /// let keyed_singleton = // { 1: 2, 2: 4, 3: 1 }
1827 /// # process
1828 /// # .source_iter(q!(vec![(1, 2), (2, 4), (3, 1)]))
1829 /// # .into_keyed()
1830 /// # .first();
1831 /// keyed_singleton.filter(q!(|&v| v > 1))
1832 /// # .entries()
1833 /// # }, |mut stream| async move {
1834 /// // { 1: 2, 2: 4 }
1835 /// # let mut results = Vec::new();
1836 /// # for _ in 0..2 {
1837 /// # results.push(stream.next().await.unwrap());
1838 /// # }
1839 /// # results.sort();
1840 /// # assert_eq!(results, vec![(1, 2), (2, 4)]);
1841 /// # }));
1842 /// # }
1843 /// ```
1844 pub fn filter<F>(
1845 self,
1846 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>> + Copy,
1847 ) -> KeyedSingleton<K, V, L, B>
1848 where
1849 F: Fn(&V) -> bool + 'a,
1850 {
1851 let f: ManualExpr<F, _> =
1852 ManualExpr::new(move |ctx: &OperatorContext<L, B::UnderlyingBound>| {
1853 f.splice_fn1_borrow_ctx(ctx)
1854 });
1855 let filter_f = q!({
1856 let orig = f;
1857 move |t: &(_, _)| orig(&t.1)
1858 })
1859 .splice_fn1_borrow_ctx::<(K, V), bool>(&OperatorContext::<L, B::UnderlyingBound>::new(
1860 &self.location,
1861 ))
1862 .into();
1863
1864 KeyedSingleton::new(
1865 self.location.clone(),
1866 HydroNode::Filter {
1867 f: filter_f,
1868 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1869 metadata: self
1870 .location
1871 .new_node_metadata(KeyedSingleton::<K, V, L, B>::collection_kind()),
1872 },
1873 )
1874 }
1875
1876 /// An operator that both filters and maps values. It yields only the key-value pairs where
1877 /// the supplied closure `f` returns `Some(value)`.
1878 ///
1879 /// The closure `f` receives each value `V` and returns `Option<U>`. If the closure returns
1880 /// `Some(new_value)`, the key-value pair `(key, new_value)` is included in the output.
1881 /// If it returns `None`, the key-value pair is filtered out.
1882 ///
1883 /// # Example
1884 /// ```rust
1885 /// # #[cfg(feature = "deploy")] {
1886 /// # use hydro_lang::prelude::*;
1887 /// # use futures::StreamExt;
1888 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1889 /// let keyed_singleton = // { 1: "42", 2: "hello", 3: "100" }
1890 /// # process
1891 /// # .source_iter(q!(vec![(1, "42"), (2, "hello"), (3, "100")]))
1892 /// # .into_keyed()
1893 /// # .first();
1894 /// keyed_singleton.filter_map(q!(|s| s.parse::<i32>().ok()))
1895 /// # .entries()
1896 /// # }, |mut stream| async move {
1897 /// // { 1: 42, 3: 100 }
1898 /// # let mut results = Vec::new();
1899 /// # for _ in 0..2 {
1900 /// # results.push(stream.next().await.unwrap());
1901 /// # }
1902 /// # results.sort();
1903 /// # assert_eq!(results, vec![(1, 42), (3, 100)]);
1904 /// # }));
1905 /// # }
1906 /// ```
1907 pub fn filter_map<F, U>(
1908 self,
1909 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>> + Copy,
1910 ) -> KeyedSingleton<K, U, L, B::EraseMonotonic>
1911 where
1912 F: Fn(V) -> Option<U> + 'a,
1913 {
1914 let f: ManualExpr<F, _> =
1915 ManualExpr::new(move |ctx: &OperatorContext<L, B::UnderlyingBound>| {
1916 f.splice_fn1_ctx(ctx)
1917 });
1918 let filter_map_f = q!({
1919 let orig = f;
1920 move |(k, v)| orig(v).map(|o| (k, o))
1921 })
1922 .splice_fn1_ctx::<(K, V), Option<(K, U)>>(&OperatorContext::<L, B::UnderlyingBound>::new(
1923 &self.location,
1924 ))
1925 .into();
1926
1927 KeyedSingleton::new(
1928 self.location.clone(),
1929 HydroNode::FilterMap {
1930 f: filter_map_f,
1931 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1932 metadata: self.location.new_node_metadata(KeyedSingleton::<
1933 K,
1934 U,
1935 L,
1936 B::EraseMonotonic,
1937 >::collection_kind()),
1938 },
1939 )
1940 }
1941
1942 /// Returns a keyed singleton with entries consisting of _new_ key-value pairs that have
1943 /// arrived since the previous batch was released.
1944 ///
1945 /// Currently, there is no `all_ticks` dual on [`KeyedSingleton`], instead you may want to use
1946 /// [`KeyedSingleton::into_keyed_stream`] then yield with [`KeyedStream::all_ticks`].
1947 ///
1948 /// # Non-Determinism
1949 /// Because this picks a batch of asynchronously added entries, each output keyed singleton
1950 /// has a non-deterministic set of key-value pairs.
1951 pub fn batch<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1952 self,
1953 tick: &Tick<L2>,
1954 _nondet: NonDet,
1955 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
1956 assert_eq!(Location::id(tick.outer()), Location::id(&self.location));
1957 KeyedSingleton::new(
1958 tick.drop_consistency(),
1959 HydroNode::Batch {
1960 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1961 metadata: tick
1962 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1963 },
1964 )
1965 }
1966}
1967
1968impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Bounded>> KeyedSingleton<K, V, Atomic<L>, B>
1969where
1970 L: Location<'a>,
1971{
1972 /// Returns a keyed singleton with entries consisting of _new_ key-value pairs that are being
1973 /// atomically processed.
1974 ///
1975 /// Currently, there is no dual to asynchronously yield back outside the tick, instead you
1976 /// should use [`KeyedSingleton::into_keyed_stream`] and yield a [`KeyedStream`].
1977 ///
1978 /// # Non-Determinism
1979 /// Because this picks a batch of asynchronously added entries, each output keyed singleton
1980 /// has a non-deterministic set of key-value pairs.
1981 pub fn batch_atomic<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1982 self,
1983 tick: &Tick<L2>,
1984 nondet: NonDet,
1985 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
1986 let _ = nondet;
1987 KeyedSingleton::new(
1988 tick.drop_consistency(),
1989 HydroNode::Batch {
1990 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1991 metadata: tick
1992 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1993 },
1994 )
1995 }
1996}
1997
1998#[cfg(test)]
1999mod tests {
2000 #[cfg(feature = "deploy")]
2001 use futures::{SinkExt, StreamExt};
2002 #[cfg(feature = "deploy")]
2003 use hydro_deploy::Deployment;
2004 #[cfg(any(feature = "deploy", feature = "sim"))]
2005 use stageleft::q;
2006
2007 #[cfg(any(feature = "deploy", feature = "sim"))]
2008 use crate::compile::builder::FlowBuilder;
2009 #[cfg(any(feature = "deploy", feature = "sim"))]
2010 use crate::location::Location;
2011 #[cfg(any(feature = "deploy", feature = "sim"))]
2012 use crate::nondet::nondet;
2013
2014 #[cfg(feature = "deploy")]
2015 #[tokio::test]
2016 async fn key_count_bounded_value() {
2017 let mut deployment = Deployment::new();
2018
2019 let mut flow = FlowBuilder::new();
2020 let node = flow.process::<()>();
2021 let external = flow.external::<()>();
2022
2023 let (input_port, input) = node.source_external_bincode(&external);
2024 let out = input
2025 .into_keyed()
2026 .first()
2027 .key_count()
2028 .sample_eager(nondet!(/** test */))
2029 .send_bincode_external(&external);
2030
2031 let nodes = flow
2032 .with_process(&node, deployment.Localhost())
2033 .with_external(&external, deployment.Localhost())
2034 .deploy(&mut deployment);
2035
2036 deployment.deploy().await.unwrap();
2037
2038 let mut external_in = nodes.connect(input_port).await;
2039 let mut external_out = nodes.connect(out).await;
2040
2041 deployment.start().await.unwrap();
2042
2043 assert_eq!(external_out.next().await.unwrap(), 0);
2044
2045 external_in.send((1, 1)).await.unwrap();
2046 assert_eq!(external_out.next().await.unwrap(), 1);
2047
2048 external_in.send((2, 2)).await.unwrap();
2049 assert_eq!(external_out.next().await.unwrap(), 2);
2050 }
2051
2052 #[cfg(feature = "deploy")]
2053 #[tokio::test]
2054 async fn key_count_unbounded_value() {
2055 let mut deployment = Deployment::new();
2056
2057 let mut flow = FlowBuilder::new();
2058 let node = flow.process::<()>();
2059 let external = flow.external::<()>();
2060
2061 let (input_port, input) = node.source_external_bincode(&external);
2062 let out = input
2063 .into_keyed()
2064 .fold(q!(|| 0), q!(|acc, _| *acc += 1))
2065 .key_count()
2066 .sample_eager(nondet!(/** test */))
2067 .send_bincode_external(&external);
2068
2069 let nodes = flow
2070 .with_process(&node, deployment.Localhost())
2071 .with_external(&external, deployment.Localhost())
2072 .deploy(&mut deployment);
2073
2074 deployment.deploy().await.unwrap();
2075
2076 let mut external_in = nodes.connect(input_port).await;
2077 let mut external_out = nodes.connect(out).await;
2078
2079 deployment.start().await.unwrap();
2080
2081 assert_eq!(external_out.next().await.unwrap(), 0);
2082
2083 external_in.send((1, 1)).await.unwrap();
2084 assert_eq!(external_out.next().await.unwrap(), 1);
2085
2086 external_in.send((1, 2)).await.unwrap();
2087 assert_eq!(external_out.next().await.unwrap(), 1);
2088
2089 external_in.send((2, 2)).await.unwrap();
2090 assert_eq!(external_out.next().await.unwrap(), 2);
2091
2092 external_in.send((1, 1)).await.unwrap();
2093 assert_eq!(external_out.next().await.unwrap(), 2);
2094
2095 external_in.send((3, 1)).await.unwrap();
2096 assert_eq!(external_out.next().await.unwrap(), 3);
2097 }
2098
2099 #[cfg(feature = "deploy")]
2100 #[tokio::test]
2101 async fn into_singleton_bounded_value() {
2102 let mut deployment = Deployment::new();
2103
2104 let mut flow = FlowBuilder::new();
2105 let node = flow.process::<()>();
2106 let external = flow.external::<()>();
2107
2108 let (input_port, input) = node.source_external_bincode(&external);
2109 let out = input
2110 .into_keyed()
2111 .first()
2112 .into_singleton()
2113 .sample_eager(nondet!(/** test */))
2114 .send_bincode_external(&external);
2115
2116 let nodes = flow
2117 .with_process(&node, deployment.Localhost())
2118 .with_external(&external, deployment.Localhost())
2119 .deploy(&mut deployment);
2120
2121 deployment.deploy().await.unwrap();
2122
2123 let mut external_in = nodes.connect(input_port).await;
2124 let mut external_out = nodes.connect(out).await;
2125
2126 deployment.start().await.unwrap();
2127
2128 assert_eq!(
2129 external_out.next().await.unwrap(),
2130 std::collections::HashMap::new()
2131 );
2132
2133 external_in.send((1, 1)).await.unwrap();
2134 assert_eq!(
2135 external_out.next().await.unwrap(),
2136 vec![(1, 1)].into_iter().collect()
2137 );
2138
2139 external_in.send((2, 2)).await.unwrap();
2140 assert_eq!(
2141 external_out.next().await.unwrap(),
2142 vec![(1, 1), (2, 2)].into_iter().collect()
2143 );
2144 }
2145
2146 #[cfg(feature = "deploy")]
2147 #[tokio::test]
2148 async fn into_singleton_unbounded_value() {
2149 let mut deployment = Deployment::new();
2150
2151 let mut flow = FlowBuilder::new();
2152 let node = flow.process::<()>();
2153 let external = flow.external::<()>();
2154
2155 let (input_port, input) = node.source_external_bincode(&external);
2156 let out = input
2157 .into_keyed()
2158 .fold(q!(|| 0), q!(|acc, _| *acc += 1))
2159 .into_singleton()
2160 .sample_eager(nondet!(/** test */))
2161 .send_bincode_external(&external);
2162
2163 let nodes = flow
2164 .with_process(&node, deployment.Localhost())
2165 .with_external(&external, deployment.Localhost())
2166 .deploy(&mut deployment);
2167
2168 deployment.deploy().await.unwrap();
2169
2170 let mut external_in = nodes.connect(input_port).await;
2171 let mut external_out = nodes.connect(out).await;
2172
2173 deployment.start().await.unwrap();
2174
2175 assert_eq!(
2176 external_out.next().await.unwrap(),
2177 std::collections::HashMap::new()
2178 );
2179
2180 external_in.send((1, 1)).await.unwrap();
2181 assert_eq!(
2182 external_out.next().await.unwrap(),
2183 vec![(1, 1)].into_iter().collect()
2184 );
2185
2186 external_in.send((1, 2)).await.unwrap();
2187 assert_eq!(
2188 external_out.next().await.unwrap(),
2189 vec![(1, 2)].into_iter().collect()
2190 );
2191
2192 external_in.send((2, 2)).await.unwrap();
2193 assert_eq!(
2194 external_out.next().await.unwrap(),
2195 vec![(1, 2), (2, 1)].into_iter().collect()
2196 );
2197
2198 external_in.send((1, 1)).await.unwrap();
2199 assert_eq!(
2200 external_out.next().await.unwrap(),
2201 vec![(1, 3), (2, 1)].into_iter().collect()
2202 );
2203
2204 external_in.send((3, 1)).await.unwrap();
2205 assert_eq!(
2206 external_out.next().await.unwrap(),
2207 vec![(1, 3), (2, 1), (3, 1)].into_iter().collect()
2208 );
2209 }
2210
2211 #[cfg(feature = "sim")]
2212 #[test]
2213 fn sim_unbounded_singleton_snapshot() {
2214 let mut flow = FlowBuilder::new();
2215 let node = flow.process::<()>();
2216
2217 let (input_port, input) = node.sim_input();
2218 let output = input
2219 .into_keyed()
2220 .fold(q!(|| 0), q!(|acc, _| *acc += 1))
2221 .snapshot(&node.tick(), nondet!(/** test */))
2222 .entries()
2223 .all_ticks()
2224 .sim_output();
2225
2226 let count = flow.sim().exhaustive(async || {
2227 input_port.send((1, 123));
2228 input_port.send((1, 456));
2229 input_port.send((2, 123));
2230
2231 let all = output.collect_sorted::<Vec<_>>().await;
2232 assert_eq!(all.last().unwrap(), &(2, 1));
2233 });
2234
2235 assert_eq!(count, 8);
2236 }
2237
2238 #[cfg(feature = "deploy")]
2239 #[tokio::test]
2240 async fn join_keyed_stream() {
2241 let mut deployment = Deployment::new();
2242
2243 let mut flow = FlowBuilder::new();
2244 let node = flow.process::<()>();
2245 let external = flow.external::<()>();
2246
2247 let tick = node.tick();
2248 let keyed_data = node
2249 .source_iter(q!(vec![(1, 10), (2, 20)]))
2250 .into_keyed()
2251 .batch(&tick, nondet!(/** test */))
2252 .first();
2253 let requests = node
2254 .source_iter(q!(vec![(1, 100), (2, 200), (3, 300)]))
2255 .into_keyed()
2256 .batch(&tick, nondet!(/** test */));
2257
2258 let out = keyed_data
2259 .join_keyed_stream(requests)
2260 .entries()
2261 .all_ticks()
2262 .send_bincode_external(&external);
2263
2264 let nodes = flow
2265 .with_process(&node, deployment.Localhost())
2266 .with_external(&external, deployment.Localhost())
2267 .deploy(&mut deployment);
2268
2269 deployment.deploy().await.unwrap();
2270
2271 let mut external_out = nodes.connect(out).await;
2272
2273 deployment.start().await.unwrap();
2274
2275 let mut results = vec![];
2276 for _ in 0..2 {
2277 results.push(external_out.next().await.unwrap());
2278 }
2279 results.sort();
2280
2281 assert_eq!(results, vec![(1, (10, 100)), (2, (20, 200))]);
2282 }
2283
2284 #[cfg(feature = "sim")]
2285 #[test]
2286 fn threshold_greater_or_equal_monotonic() {
2287 let mut flow = FlowBuilder::new();
2288 let node = flow.process::<()>();
2289
2290 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2291 let (thresh_port, thresh_input) = node.sim_input::<(u32, usize), _, _>();
2292
2293 // Create a monotonically increasing keyed singleton via fold with monotone proof
2294 let counts: super::KeyedSingleton<u32, usize, _, super::MonotonicValue> =
2295 input.into_keyed().fold(
2296 q!(|| 0usize),
2297 q!(
2298 |acc, v| *acc += v,
2299 monotone = crate::properties::manual_proof!(/** += is monotonic */)
2300 ),
2301 );
2302
2303 // BoundedValue keyed singleton of thresholds (from .first() on unbounded stream)
2304 let thresholds = thresh_input.into_keyed().first();
2305
2306 let output = counts
2307 .threshold_greater_or_equal(thresholds)
2308 .entries()
2309 .sim_output();
2310
2311 let count = flow.sim().exhaustive(async || {
2312 // Set thresholds: key 1 needs value >= 5, key 2 needs value >= 10
2313 thresh_port.send((1, 5));
2314 thresh_port.send((2, 10));
2315
2316 // key 1 gets increments: 3 + 3 = 6, which is >= 5 ✓
2317 input_port.send((1, 3));
2318 input_port.send((1, 3));
2319 // key 2 gets increments: 3 + 3 = 6, which is < 10 ✗
2320 input_port.send((2, 3));
2321 input_port.send((2, 3));
2322
2323 let results = output.collect_sorted::<Vec<_>>().await;
2324 assert_eq!(results, vec![(1, 5)]);
2325 });
2326
2327 assert!(count > 0);
2328 }
2329
2330 #[cfg(feature = "sim")]
2331 #[test]
2332 fn threshold_greater_or_equal_uniform() {
2333 let mut flow = FlowBuilder::new();
2334 let node = flow.process::<()>();
2335
2336 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2337
2338 let counts: super::KeyedSingleton<u32, usize, _, super::MonotonicValue> =
2339 input.into_keyed().fold(
2340 q!(|| 0usize),
2341 q!(
2342 |acc, v| *acc += v,
2343 monotone = crate::properties::manual_proof!(/** += is monotonic */)
2344 ),
2345 );
2346
2347 // Uniform threshold: all keys need value >= 5
2348 let threshold = node.singleton(q!(5usize));
2349
2350 let output = counts
2351 .threshold_greater_or_equal_uniform(threshold)
2352 .entries()
2353 .sim_output();
2354
2355 let count = flow.sim().exhaustive(async || {
2356 // key 1: 3 + 3 = 6 >= 5 ✓
2357 input_port.send((1, 3));
2358 input_port.send((1, 3));
2359 // key 2: 2 + 2 = 4 < 5 ✗
2360 input_port.send((2, 2));
2361 input_port.send((2, 2));
2362
2363 let results = output.collect_sorted::<Vec<_>>().await;
2364 assert_eq!(results, vec![(1, 5)]);
2365 });
2366
2367 assert!(count > 0);
2368 }
2369
2370 #[cfg(feature = "sim")]
2371 #[test]
2372 fn threshold_greater_or_equal_bounded_value() {
2373 let mut flow = FlowBuilder::new();
2374 let node = flow.process::<()>();
2375
2376 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2377 let (thresh_port, thresh_input) = node.sim_input::<(u32, usize), _, _>();
2378
2379 // BoundedValue keyed singleton (values fixed once per key via .first())
2380 let values = input.into_keyed().first();
2381
2382 // BoundedValue keyed singleton of thresholds
2383 let thresholds = thresh_input.into_keyed().first();
2384
2385 let output = values
2386 .threshold_greater_or_equal(thresholds)
2387 .entries()
2388 .sim_output();
2389
2390 let count = flow.sim().exhaustive(async || {
2391 // Set thresholds: key 1 needs >= 3, key 2 needs >= 10
2392 thresh_port.send((1, 3));
2393 thresh_port.send((2, 10));
2394
2395 // key 1 gets value 5 >= 3 ✓, key 2 gets value 4 < 10 ✗
2396 input_port.send((1, 5));
2397 input_port.send((2, 4));
2398
2399 let results = output.collect_sorted::<Vec<_>>().await;
2400 assert_eq!(results, vec![(1, 3)]);
2401 });
2402
2403 assert!(count > 0);
2404 }
2405
2406 #[cfg(feature = "sim")]
2407 #[test]
2408 fn threshold_greater_or_equal_uniform_bounded_value() {
2409 let mut flow = FlowBuilder::new();
2410 let node = flow.process::<()>();
2411
2412 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2413
2414 // BoundedValue keyed singleton (values fixed once per key via .first())
2415 let values = input.into_keyed().first();
2416
2417 // Uniform threshold: all keys need value >= 5
2418 let threshold = node.singleton(q!(5usize));
2419
2420 let output = values
2421 .threshold_greater_or_equal_uniform(threshold)
2422 .entries()
2423 .sim_output();
2424
2425 let count = flow.sim().exhaustive(async || {
2426 // key 1 gets value 7 >= 5 ✓, key 2 gets value 3 < 5 ✗
2427 input_port.send((1, 7));
2428 input_port.send((2, 3));
2429
2430 let results = output.collect_sorted::<Vec<_>>().await;
2431 assert_eq!(results, vec![(1, 5)]);
2432 });
2433
2434 assert!(count > 0);
2435 }
2436
2437 #[cfg(feature = "sim")]
2438 #[test]
2439 fn threshold_greater_or_equal_bounded() {
2440 let mut flow = FlowBuilder::new();
2441 let node = flow.process::<()>();
2442
2443 // Bounded keyed singleton (fully known upfront)
2444 let values = node
2445 .source_iter(q!(vec![(1, 6usize), (2, 4usize)]))
2446 .into_keyed()
2447 .first();
2448
2449 // BoundedValue thresholds (from async source)
2450 let (thresh_port, thresh_input) = node.sim_input::<(u32, usize), _, _>();
2451 let thresholds = thresh_input.into_keyed().first();
2452
2453 let output = values
2454 .threshold_greater_or_equal(thresholds)
2455 .entries()
2456 .sim_output();
2457
2458 let count = flow.sim().exhaustive(async || {
2459 thresh_port.send((1, 5));
2460 thresh_port.send((2, 10));
2461
2462 // key 1: 6 >= 5 ✓, key 2: 4 < 10 ✗
2463 let results = output.collect_sorted::<Vec<_>>().await;
2464 assert_eq!(results, vec![(1, 5)]);
2465 });
2466
2467 assert!(count > 0);
2468 }
2469
2470 #[cfg(feature = "sim")]
2471 #[test]
2472 fn threshold_greater_or_equal_uniform_bounded() {
2473 let mut flow = FlowBuilder::new();
2474 let node = flow.process::<()>();
2475
2476 let values = node
2477 .source_iter(q!(vec![(1, 6usize), (2, 4usize)]))
2478 .into_keyed()
2479 .first();
2480 let threshold = node.singleton(q!(5usize));
2481
2482 let output = values
2483 .threshold_greater_or_equal_uniform(threshold)
2484 .entries()
2485 .sim_output();
2486
2487 let count = flow.sim().exhaustive(async || {
2488 // key 1: 6 >= 5 ✓, key 2: 4 < 5 ✗
2489 let results = output.collect_sorted::<Vec<_>>().await;
2490 assert_eq!(results, vec![(1, 5)]);
2491 });
2492
2493 assert!(count > 0);
2494 }
2495}