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, TopLevel, 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 enum 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 enum 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 enum 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 = key_count_inside_tick(me.snapshot(&tick, nondet!(/** eventually stabilizes */)))
640 .latest()
641 // The key count is folded with an initial value, so it is always present
642 // (0 when there are no keys). `latest()` is null until the producing tick
643 // first runs; fill that prefix with 0 to recover an always-present count.
644 .unwrap_or(location.singleton(q!(0usize)).into());
645 // Re-tag the node from the concrete `Unbounded` singleton to the `B::UnderlyingBound`
646 // that this method returns (equal at runtime for this branch).
647 Singleton::new(location, out.ir_node.replace(HydroNode::Placeholder))
648 } else {
649 panic!("BoundedValue or Unbounded KeyedSingleton inside a tick, not supported");
650 }
651 }
652
653 /// Converts this keyed singleton into a [`Singleton`] containing a `HashMap` from keys to values.
654 ///
655 /// As the values for each key are updated asynchronously, the `HashMap` will be updated
656 /// asynchronously as well.
657 ///
658 /// # Example
659 /// ```rust
660 /// # #[cfg(feature = "deploy")] {
661 /// # use hydro_lang::prelude::*;
662 /// # use futures::StreamExt;
663 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
664 /// let keyed_singleton = // { 1: "a", 2: "b", 3: "c" }
665 /// # process
666 /// # .source_iter(q!(vec![(1, "a".to_owned()), (2, "b".to_owned()), (3, "c".to_owned())]))
667 /// # .into_keyed()
668 /// # .batch(&process.tick(), nondet!(/** test */))
669 /// # .first();
670 /// keyed_singleton.into_singleton()
671 /// # .all_ticks()
672 /// # }, |mut stream| async move {
673 /// // { 1: "a", 2: "b", 3: "c" }
674 /// # assert_eq!(stream.next().await.unwrap(), vec![(1, "a".to_owned()), (2, "b".to_owned()), (3, "c".to_owned())].into_iter().collect());
675 /// # }));
676 /// # }
677 /// ```
678 pub fn into_singleton(self) -> Singleton<HashMap<K, V>, L, B::UnderlyingBound>
679 where
680 K: Eq + Hash + Clone + 'a,
681 V: Clone + 'a,
682 {
683 if B::ValueBound::BOUNDED {
684 let me: KeyedSingleton<K, V, L, B::WithBoundedValue> = KeyedSingleton {
685 location: self.location.clone(),
686 flow_state: self.flow_state.clone(),
687 ir_node: super::tracked_ir_node(
688 &self.flow_state,
689 self.ir_node.replace(HydroNode::Placeholder),
690 ),
691 _phantom: PhantomData,
692 };
693
694 me.entries()
695 .assume_ordering_trusted(nondet!(
696 /// There is only one element associated with each key. The closure technically
697 /// isn't commutative in the case where both passed entries have the same key
698 /// but different values.
699 ///
700 /// In the future, we may want to have an `assume!(...)` statement in the UDF that
701 /// the key is never already present in the map.
702 ))
703 .fold(
704 q!(|| HashMap::new()),
705 q!(|map, (k, v)| {
706 // TODO(shadaj): make this commutative but really-debug-assert that there is no key overlap
707 map.insert(k, v);
708 }),
709 )
710 } else if L::is_top_level()
711 && let Some(tick) = self.location.try_tick()
712 && (B::bound_kind() == KeyedSingletonBoundKind::Unbounded
713 || B::bound_kind() == KeyedSingletonBoundKind::MonotonicKeys
714 || B::bound_kind() == KeyedSingletonBoundKind::MonotonicValue)
715 {
716 let location = self.location.clone();
717 let ir_node = self.ir_node.replace(HydroNode::Placeholder);
718 let me: KeyedSingleton<K, V, L, MonotonicKeys> =
719 KeyedSingleton::new(location.clone(), ir_node);
720
721 let out = into_singleton_inside_tick(
722 me.snapshot(&tick, nondet!(/** eventually stabilizes */)),
723 )
724 .latest()
725 // The map is folded with an initial value, so it is always present (empty when
726 // there are no keys). `latest()` is null until the producing tick first runs; fill
727 // that prefix with an empty map to recover an always-present map.
728 .unwrap_or(location.singleton(q!(HashMap::new())).into());
729 // Re-tag the node from the concrete `Unbounded` singleton to the `B::UnderlyingBound`
730 // that this method returns (equal at runtime for this branch).
731 Singleton::new(location, out.ir_node.replace(HydroNode::Placeholder))
732 } else {
733 panic!("BoundedValue or Unbounded KeyedSingleton inside a tick, not supported");
734 }
735 }
736
737 /// An operator which allows you to "name" a `HydroNode`.
738 /// This is only used for testing, to correlate certain `HydroNode`s with IDs.
739 pub fn ir_node_named(self, name: &str) -> KeyedSingleton<K, V, L, B> {
740 {
741 let mut node = self.ir_node.borrow_mut();
742 let metadata = node.metadata_mut();
743 metadata.tag = Some(name.to_owned());
744 }
745 self
746 }
747
748 /// Strengthens the boundedness guarantee to `Bounded`, given that `B: IsBounded`, which
749 /// implies that `B == Bounded`.
750 pub fn make_bounded(self) -> KeyedSingleton<K, V, L, Bounded>
751 where
752 B: IsBounded,
753 {
754 KeyedSingleton::new(
755 self.location.clone(),
756 self.ir_node.replace(HydroNode::Placeholder),
757 )
758 }
759
760 /// Gets the value associated with a specific key from the keyed singleton.
761 /// Returns `None` if the key is `None` or there is no associated value.
762 ///
763 /// # Example
764 /// ```rust
765 /// # #[cfg(feature = "deploy")] {
766 /// # use hydro_lang::prelude::*;
767 /// # use futures::StreamExt;
768 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
769 /// let tick = process.tick();
770 /// let keyed_data = process
771 /// .source_iter(q!(vec![(1, 2), (2, 3)]))
772 /// .into_keyed()
773 /// .batch(&tick, nondet!(/** test */))
774 /// .first();
775 /// let key = tick.singleton(q!(1));
776 /// keyed_data.get(key).all_ticks()
777 /// # }, |mut stream| async move {
778 /// // 2
779 /// # assert_eq!(stream.next().await.unwrap(), 2);
780 /// # }));
781 /// # }
782 /// ```
783 pub fn get(self, key: impl Into<Optional<K, L, Bounded>>) -> Optional<V, L, Bounded>
784 where
785 B: IsBounded,
786 K: Hash + Eq + Clone,
787 V: Clone,
788 {
789 self.make_bounded()
790 .into_keyed_stream()
791 .get(key)
792 .cast_at_most_one_element()
793 }
794
795 /// Emit a keyed stream containing keys shared between the keyed singleton and the
796 /// keyed stream, where each value in the output keyed stream is a tuple of
797 /// (the keyed singleton's value, the keyed stream's value).
798 ///
799 /// # Example
800 /// ```rust
801 /// # #[cfg(feature = "deploy")] {
802 /// # use hydro_lang::prelude::*;
803 /// # use futures::StreamExt;
804 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
805 /// let tick = process.tick();
806 /// let keyed_data = process
807 /// .source_iter(q!(vec![(1, 10), (2, 20)]))
808 /// .into_keyed()
809 /// .batch(&tick, nondet!(/** test */))
810 /// .first();
811 /// let other_data = process
812 /// .source_iter(q!(vec![(1, 100), (2, 200), (1, 101)]))
813 /// .into_keyed()
814 /// .batch(&tick, nondet!(/** test */));
815 /// keyed_data.join_keyed_stream(other_data).entries().all_ticks()
816 /// # }, |mut stream| async move {
817 /// // { 1: [(10, 100), (10, 101)], 2: [(20, 200)] } in any order
818 /// # let mut results = vec![];
819 /// # for _ in 0..3 {
820 /// # results.push(stream.next().await.unwrap());
821 /// # }
822 /// # results.sort();
823 /// # assert_eq!(results, vec![(1, (10, 100)), (1, (10, 101)), (2, (20, 200))]);
824 /// # }));
825 /// # }
826 /// ```
827 pub fn join_keyed_stream<O2: Ordering, R2: Retries, V2, B2: Boundedness>(
828 self,
829 other: KeyedStream<K, V2, L, B2, O2, R2>,
830 ) -> KeyedStream<K, (V, V2), L, B2, O2, R2>
831 where
832 B: IsBounded,
833 K: Eq + Hash + Clone,
834 V: Clone,
835 V2: Clone,
836 {
837 // TODO(shadaj): if DFIR guarantees that joining unbounded keyed stream x bounded keyed stream
838 // always produces deterministic order per key (nested loop join), this could just use
839 // `join_keyed_stream` without constructing IRs manually
840 KeyedStream::new(
841 self.location.clone(),
842 HydroNode::Join {
843 left: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
844 right: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
845 metadata: self
846 .location
847 .new_node_metadata(KeyedStream::<K, (V, V2), L, B2, O2, R2>::collection_kind()),
848 },
849 )
850 }
851
852 /// Emit a keyed singleton containing all keys shared between two keyed singletons,
853 /// where each value in the output keyed singleton is a tuple of
854 /// (self.value, other.value).
855 ///
856 /// # Example
857 /// ```rust
858 /// # #[cfg(feature = "deploy")] {
859 /// # use hydro_lang::prelude::*;
860 /// # use futures::StreamExt;
861 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
862 /// # let tick = process.tick();
863 /// let requests = // { 1: 10, 2: 20, 3: 30 }
864 /// # process
865 /// # .source_iter(q!(vec![(1, 10), (2, 20), (3, 30)]))
866 /// # .into_keyed()
867 /// # .batch(&tick, nondet!(/** test */))
868 /// # .first();
869 /// let other = // { 1: 100, 2: 200, 4: 400 }
870 /// # process
871 /// # .source_iter(q!(vec![(1, 100), (2, 200), (4, 400)]))
872 /// # .into_keyed()
873 /// # .batch(&tick, nondet!(/** test */))
874 /// # .first();
875 /// requests.join_keyed_singleton(other)
876 /// # .entries().all_ticks()
877 /// # }, |mut stream| async move {
878 /// // { 1: (10, 100), 2: (20, 200) }
879 /// # let mut results = vec![];
880 /// # for _ in 0..2 {
881 /// # results.push(stream.next().await.unwrap());
882 /// # }
883 /// # results.sort();
884 /// # assert_eq!(results, vec![(1, (10, 100)), (2, (20, 200))]);
885 /// # }));
886 /// # }
887 /// ```
888 pub fn join_keyed_singleton<V2: Clone>(
889 self,
890 other: KeyedSingleton<K, V2, L, Bounded>,
891 ) -> KeyedSingleton<K, (V, V2), L, Bounded>
892 where
893 B: IsBounded,
894 K: Eq + Hash + Clone,
895 V: Clone,
896 {
897 let result_stream = self
898 .make_bounded()
899 .entries()
900 .join(other.entries())
901 .into_keyed();
902
903 // The cast is guaranteed to succeed, since each key (in both `self` and `other`) has at most one value.
904 result_stream.cast_at_most_one_entry_per_key()
905 }
906
907 /// For each value in `self`, find the matching key in `lookup`.
908 /// The output is a keyed singleton with the key from `self`, and a value
909 /// that is a tuple of (`self`'s value, Option<`lookup`'s value>).
910 /// If the key is not present in `lookup`, the option will be [`None`].
911 ///
912 /// # Example
913 /// ```rust
914 /// # #[cfg(feature = "deploy")] {
915 /// # use hydro_lang::prelude::*;
916 /// # use futures::StreamExt;
917 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
918 /// # let tick = process.tick();
919 /// let requests = // { 1: 10, 2: 20 }
920 /// # process
921 /// # .source_iter(q!(vec![(1, 10), (2, 20)]))
922 /// # .into_keyed()
923 /// # .batch(&tick, nondet!(/** test */))
924 /// # .first();
925 /// let other_data = // { 10: 100, 11: 110 }
926 /// # process
927 /// # .source_iter(q!(vec![(10, 100), (11, 110)]))
928 /// # .into_keyed()
929 /// # .batch(&tick, nondet!(/** test */))
930 /// # .first();
931 /// requests.lookup_keyed_singleton(other_data)
932 /// # .entries().all_ticks()
933 /// # }, |mut stream| async move {
934 /// // { 1: (10, Some(100)), 2: (20, None) }
935 /// # let mut results = vec![];
936 /// # for _ in 0..2 {
937 /// # results.push(stream.next().await.unwrap());
938 /// # }
939 /// # results.sort();
940 /// # assert_eq!(results, vec![(1, (10, Some(100))), (2, (20, None))]);
941 /// # }));
942 /// # }
943 /// ```
944 pub fn lookup_keyed_singleton<V2>(
945 self,
946 lookup: KeyedSingleton<V, V2, L, Bounded>,
947 ) -> KeyedSingleton<K, (V, Option<V2>), L, Bounded>
948 where
949 B: IsBounded,
950 K: Eq + Hash + Clone,
951 V: Eq + Hash + Clone,
952 V2: Clone,
953 {
954 let result_stream = self
955 .make_bounded()
956 .into_keyed_stream()
957 .lookup_keyed_stream(lookup.into_keyed_stream());
958
959 // The cast is guaranteed to succeed since both lookup and self contain at most 1 value per key
960 result_stream.cast_at_most_one_entry_per_key()
961 }
962
963 /// For each value in `self`, find the matching key in `lookup`.
964 /// The output is a keyed stream with the key from `self`, and a value
965 /// that is a tuple of (`self`'s value, Option<`lookup`'s value>).
966 /// If the key is not present in `lookup`, the option will be [`None`].
967 ///
968 /// # Example
969 /// ```rust
970 /// # #[cfg(feature = "deploy")] {
971 /// # use hydro_lang::prelude::*;
972 /// # use futures::StreamExt;
973 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
974 /// # let tick = process.tick();
975 /// let requests = // { 1: 10, 2: 20 }
976 /// # process
977 /// # .source_iter(q!(vec![(1, 10), (2, 20)]))
978 /// # .into_keyed()
979 /// # .batch(&tick, nondet!(/** test */))
980 /// # .first();
981 /// let other_data = // { 10: 100, 10: 110 }
982 /// # process
983 /// # .source_iter(q!(vec![(10, 100), (10, 110)]))
984 /// # .into_keyed()
985 /// # .batch(&tick, nondet!(/** test */));
986 /// requests.lookup_keyed_stream(other_data)
987 /// # .entries().all_ticks()
988 /// # }, |mut stream| async move {
989 /// // { 1: [(10, Some(100)), (10, Some(110))], 2: (20, None) }
990 /// # let mut results = vec![];
991 /// # for _ in 0..3 {
992 /// # results.push(stream.next().await.unwrap());
993 /// # }
994 /// # results.sort();
995 /// # assert_eq!(results, vec![(1, (10, Some(100))), (1, (10, Some(110))), (2, (20, None))]);
996 /// # }));
997 /// # }
998 /// ```
999 pub fn lookup_keyed_stream<V2, O: Ordering, R: Retries>(
1000 self,
1001 lookup: KeyedStream<V, V2, L, Bounded, O, R>,
1002 ) -> KeyedStream<K, (V, Option<V2>), L, Bounded, NoOrder, R>
1003 where
1004 B: IsBounded,
1005 K: Eq + Hash + Clone,
1006 V: Eq + Hash + Clone,
1007 V2: Clone,
1008 {
1009 self.make_bounded()
1010 .entries()
1011 .weaken_retries::<R>() // TODO: Once weaken_retries() is implemented for KeyedSingleton, remove entries() and into_keyed()
1012 .into_keyed()
1013 .lookup_keyed_stream(lookup)
1014 }
1015
1016 /// For each key present in both `self` and `thresholds`, emits a [`KeyedStream`] event the first
1017 /// time that key's value becomes greater than or equal to the corresponding threshold value.
1018 /// The emitted value for each key is the threshold value itself.
1019 ///
1020 /// This requires the keyed singleton to have monotonic values ([`MonotonicValue`] or [`Bounded`]),
1021 /// because otherwise the threshold detection would be non-deterministic.
1022 ///
1023 /// The `thresholds` parameter is a [`BoundedValue`] keyed singleton mapping each key to its
1024 /// threshold. Thresholds may arrive asynchronously (new keys appear over time), but once set
1025 /// for a key, the threshold value is fixed. Late-arriving thresholds are checked against the
1026 /// current snapshot value immediately.
1027 ///
1028 /// # Example
1029 /// ```rust,ignore
1030 /// use hydro_lang::prelude::*;
1031 ///
1032 /// // Given a monotonically increasing keyed singleton (e.g. from fold with monotone proof)
1033 /// let counts: KeyedSingleton<u32, usize, _, MonotonicValue> = events.into_keyed()
1034 /// .fold(q!(|| 0), q!(|acc, _| *acc += 1, monotone = manual_proof!(/** +1 is monotone */)));
1035 ///
1036 /// // BoundedValue keyed singleton of thresholds (from .first())
1037 /// let thresholds = threshold_source.into_keyed().first();
1038 ///
1039 /// // Emits (key, threshold_value) the first time each key's value >= threshold
1040 /// let crossed = counts.threshold_greater_or_equal(thresholds);
1041 /// ```
1042 pub fn threshold_greater_or_equal(
1043 self,
1044 thresholds: KeyedSingleton<K, V, L, BoundedValue>,
1045 ) -> KeyedStream<K, V, L, B::UnderlyingBound, NoOrder, ExactlyOnce>
1046 where
1047 K: Clone + Eq + Hash,
1048 V: Clone + PartialOrd,
1049 B: IsKeyedMonotonic,
1050 {
1051 let self_location = self.location.clone();
1052 match B::bound_kind() {
1053 KeyedSingletonBoundKind::Bounded => {
1054 // Bounded case: self is already fixed, just join and filter
1055 let me: KeyedSingleton<K, V, L, Bounded> = KeyedSingleton::new(
1056 self.location.clone(),
1057 self.ir_node.replace(HydroNode::Placeholder),
1058 );
1059 let result = me
1060 .entries()
1061 .join(thresholds.entries())
1062 .filter_map(q!(|(k, (val, thresh))| {
1063 if val >= thresh {
1064 Some((k, thresh))
1065 } else {
1066 None
1067 }
1068 }))
1069 .into_keyed();
1070 KeyedStream::new(
1071 result.location.clone(),
1072 result.ir_node.replace(HydroNode::Placeholder),
1073 )
1074 }
1075 KeyedSingletonBoundKind::MonotonicValue => {
1076 let me: KeyedSingleton<K, V, L, MonotonicValue> = KeyedSingleton::new(
1077 self.location.clone(),
1078 self.ir_node.replace(HydroNode::Placeholder),
1079 );
1080
1081 let result = sliced! {
1082 let snapshot = use::snapshot(me, nondet!(/** thresholds are deterministic */));
1083 let thresh_snapshot =
1084 use::batch(thresholds, nondet!(/** thresholds are deterministic */));
1085 let mut already_crossed =
1086 use::state_null::<Stream<K, Tick<_>, Bounded, NoOrder>>();
1087
1088 let joined = thresh_snapshot.entries().join(snapshot.entries());
1089 let passed = joined
1090 .filter(q!(|(_, (thresh, val))| *val >= *thresh))
1091 .map(q!(|(k, (thresh, _))| (k, thresh)));
1092
1093 let newly_crossed = passed.anti_join(already_crossed.clone());
1094 already_crossed =
1095 already_crossed.chain(newly_crossed.clone().map(q!(|(k, _)| k)));
1096
1097 newly_crossed.into_keyed()
1098 };
1099
1100 KeyedStream::new(
1101 self_location,
1102 result.ir_node.replace(HydroNode::Placeholder),
1103 )
1104 }
1105 KeyedSingletonBoundKind::BoundedValue => {
1106 let me: KeyedSingleton<K, V, L, BoundedValue> = KeyedSingleton::new(
1107 self.location.clone(),
1108 self.ir_node.replace(HydroNode::Placeholder),
1109 );
1110
1111 let result = sliced! {
1112 let snapshot = use::batch(me, nondet!(/** thresholds are deterministic */));
1113 let thresh_snapshot =
1114 use::batch(thresholds, nondet!(/** thresholds are deterministic */));
1115 let mut already_crossed =
1116 use::state_null::<Stream<K, Tick<_>, Bounded, NoOrder>>();
1117
1118 let joined = thresh_snapshot.entries().join(snapshot.entries());
1119 let passed = joined
1120 .filter(q!(|(_, (thresh, val))| *val >= *thresh))
1121 .map(q!(|(k, (thresh, _))| (k, thresh)));
1122
1123 let newly_crossed = passed.anti_join(already_crossed.clone());
1124 already_crossed =
1125 already_crossed.chain(newly_crossed.clone().map(q!(|(k, _)| k)));
1126
1127 newly_crossed.into_keyed()
1128 };
1129
1130 KeyedStream::new(
1131 self_location,
1132 result.ir_node.replace(HydroNode::Placeholder),
1133 )
1134 }
1135 _ => {
1136 unreachable!(
1137 "IsKeyedMonotonic is only implemented for Bounded, BoundedValue, and MonotonicValue"
1138 )
1139 }
1140 }
1141 }
1142
1143 /// Like [`Self::threshold_greater_or_equal`], but uses a single [`Singleton`] threshold
1144 /// shared across all keys. Emits a `(K, V)` event for each key the first time that key's
1145 /// value becomes >= the threshold. The emitted value is the threshold itself.
1146 ///
1147 /// Because the threshold is a [`Bounded`] singleton, it is a compile-time constant and
1148 /// does not carry ongoing memory cost.
1149 ///
1150 /// # Example
1151 /// ```rust
1152 /// # #[cfg(feature = "deploy")] {
1153 /// # use hydro_lang::prelude::*;
1154 /// # use futures::StreamExt;
1155 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1156 /// // A keyed singleton of per-key values (in practice often a monotone counter): { 1: 6, 2: 4 }
1157 /// let counts = process
1158 /// .source_iter(q!(vec![(1, 6), (2, 4)]))
1159 /// .into_keyed()
1160 /// .first();
1161 ///
1162 /// // A single threshold value shared across all keys
1163 /// let threshold = process.singleton(q!(5));
1164 ///
1165 /// // Emits (key, threshold) the first time each key's value >= threshold
1166 /// counts.threshold_greater_or_equal_uniform(threshold)
1167 /// # .entries()
1168 /// # }, |mut stream| async move {
1169 /// // { 1: 5 } -- key 1's value 6 >= 5, but key 2's value 4 < 5
1170 /// # assert_eq!(stream.next().await.unwrap(), (1, 5));
1171 /// # }));
1172 /// # }
1173 /// ```
1174 pub fn threshold_greater_or_equal_uniform(
1175 self,
1176 threshold: Singleton<V, L, Bounded>,
1177 ) -> KeyedStream<K, V, L, B::UnderlyingBound, NoOrder, ExactlyOnce>
1178 where
1179 K: Clone + Eq + Hash,
1180 V: Clone + PartialOrd,
1181 B: IsKeyedMonotonic,
1182 {
1183 let self_location = self.location.clone();
1184 match B::bound_kind() {
1185 KeyedSingletonBoundKind::Bounded => {
1186 let me: KeyedSingleton<K, V, L, Bounded> = KeyedSingleton::new(
1187 self.location.clone(),
1188 self.ir_node.replace(HydroNode::Placeholder),
1189 );
1190 let result = me
1191 .entries()
1192 .cross_singleton(threshold)
1193 .filter_map(q!(|((k, val), thresh)| {
1194 if val >= thresh {
1195 Some((k, thresh))
1196 } else {
1197 None
1198 }
1199 }))
1200 .into_keyed();
1201 KeyedStream::new(
1202 result.location.clone(),
1203 result.ir_node.replace(HydroNode::Placeholder),
1204 )
1205 }
1206 KeyedSingletonBoundKind::MonotonicValue => {
1207 let me: KeyedSingleton<K, V, L, MonotonicValue> = KeyedSingleton::new(
1208 self.location.clone(),
1209 self.ir_node.replace(HydroNode::Placeholder),
1210 );
1211
1212 let result = sliced! {
1213 let snapshot = use::snapshot(me, nondet!(/** thresholds are deterministic */));
1214 let mut already_crossed =
1215 use::state_null::<Stream<K, Tick<_>, Bounded, NoOrder>>();
1216
1217 let tick = snapshot.location().clone();
1218 let thresh_in_tick = threshold.clone_into_tick(&tick);
1219
1220 let crossing = snapshot
1221 .entries()
1222 .cross_singleton(thresh_in_tick)
1223 .filter_map(q!(|((k, val), thresh)| {
1224 if val >= thresh {
1225 Some((k, thresh))
1226 } else {
1227 None
1228 }
1229 }));
1230
1231 let newly_crossed = crossing.anti_join(already_crossed.clone());
1232 already_crossed =
1233 already_crossed.chain(newly_crossed.clone().map(q!(|(k, _)| k)));
1234
1235 newly_crossed.into_keyed()
1236 };
1237
1238 KeyedStream::new(
1239 self_location,
1240 result.ir_node.replace(HydroNode::Placeholder),
1241 )
1242 }
1243 KeyedSingletonBoundKind::BoundedValue => {
1244 let me: KeyedSingleton<K, V, L, BoundedValue> = KeyedSingleton::new(
1245 self.location.clone(),
1246 self.ir_node.replace(HydroNode::Placeholder),
1247 );
1248
1249 let result = sliced! {
1250 let snapshot = use::batch(me, nondet!(/** thresholds are deterministic */));
1251 let mut already_crossed =
1252 use::state_null::<Stream<K, Tick<_>, Bounded, NoOrder>>();
1253
1254 let tick = snapshot.location().clone();
1255 let thresh_in_tick = threshold.clone_into_tick(&tick);
1256
1257 let crossing = snapshot
1258 .entries()
1259 .cross_singleton(thresh_in_tick)
1260 .filter_map(q!(|((k, val), thresh)| {
1261 if val >= thresh {
1262 Some((k, thresh))
1263 } else {
1264 None
1265 }
1266 }));
1267
1268 let newly_crossed = crossing.anti_join(already_crossed.clone());
1269 already_crossed =
1270 already_crossed.chain(newly_crossed.clone().map(q!(|(k, _)| k)));
1271
1272 newly_crossed.into_keyed()
1273 };
1274
1275 KeyedStream::new(
1276 self_location,
1277 result.ir_node.replace(HydroNode::Placeholder),
1278 )
1279 }
1280 _ => {
1281 unreachable!(
1282 "IsKeyedMonotonic is only implemented for Bounded, BoundedValue, and MonotonicValue"
1283 )
1284 }
1285 }
1286 }
1287}
1288
1289impl<'a, K, V, L: Location<'a>, B: KeyedSingletonBound<ValueBound = Bounded>>
1290 KeyedSingleton<K, V, L, B>
1291{
1292 /// Flattens the keyed singleton into an unordered stream of key-value pairs.
1293 ///
1294 /// The value for each key must be bounded, otherwise the resulting stream elements would be
1295 /// non-deterministic. As new entries are added to the keyed singleton, they will be streamed
1296 /// into the output.
1297 ///
1298 /// # Example
1299 /// ```rust
1300 /// # #[cfg(feature = "deploy")] {
1301 /// # use hydro_lang::prelude::*;
1302 /// # use futures::StreamExt;
1303 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1304 /// let keyed_singleton = // { 1: 2, 2: 4 }
1305 /// # process
1306 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1307 /// # .into_keyed()
1308 /// # .first();
1309 /// keyed_singleton.entries()
1310 /// # }, |mut stream| async move {
1311 /// // (1, 2), (2, 4) in any order
1312 /// # let mut results = Vec::new();
1313 /// # for _ in 0..2 {
1314 /// # results.push(stream.next().await.unwrap());
1315 /// # }
1316 /// # results.sort();
1317 /// # assert_eq!(results, vec![(1, 2), (2, 4)]);
1318 /// # }));
1319 /// # }
1320 /// ```
1321 pub fn entries(self) -> Stream<(K, V), L, B::UnderlyingBound, NoOrder, ExactlyOnce> {
1322 self.into_keyed_stream().entries()
1323 }
1324
1325 /// Flattens the keyed singleton into an unordered stream of just the values.
1326 ///
1327 /// The value for each key must be bounded, otherwise the resulting stream elements would be
1328 /// non-deterministic. As new entries are added to the keyed singleton, they will be streamed
1329 /// into the output.
1330 ///
1331 /// # Example
1332 /// ```rust
1333 /// # #[cfg(feature = "deploy")] {
1334 /// # use hydro_lang::prelude::*;
1335 /// # use futures::StreamExt;
1336 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1337 /// let keyed_singleton = // { 1: 2, 2: 4 }
1338 /// # process
1339 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1340 /// # .into_keyed()
1341 /// # .first();
1342 /// keyed_singleton.values()
1343 /// # }, |mut stream| async move {
1344 /// // 2, 4 in any order
1345 /// # let mut results = Vec::new();
1346 /// # for _ in 0..2 {
1347 /// # results.push(stream.next().await.unwrap());
1348 /// # }
1349 /// # results.sort();
1350 /// # assert_eq!(results, vec![2, 4]);
1351 /// # }));
1352 /// # }
1353 /// ```
1354 pub fn values(self) -> Stream<V, L, B::UnderlyingBound, NoOrder, ExactlyOnce> {
1355 let map_f = q!(|(_, v)| v)
1356 .splice_fn1_ctx::<(K, V), V>(&OperatorContext::<L, B::UnderlyingBound>::new(
1357 &self.location,
1358 ))
1359 .into();
1360
1361 Stream::new(
1362 self.location.clone(),
1363 HydroNode::Map {
1364 f: map_f,
1365 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1366 metadata: self.location.new_node_metadata(Stream::<
1367 V,
1368 L,
1369 B::UnderlyingBound,
1370 NoOrder,
1371 ExactlyOnce,
1372 >::collection_kind()),
1373 },
1374 )
1375 }
1376
1377 /// Flattens the keyed singleton into an unordered stream of just the keys.
1378 ///
1379 /// The value for each key must be bounded, otherwise the removal of keys would result in
1380 /// non-determinism. As new entries are added to the keyed singleton, they will be streamed
1381 /// into the output.
1382 ///
1383 /// # Example
1384 /// ```rust
1385 /// # #[cfg(feature = "deploy")] {
1386 /// # use hydro_lang::prelude::*;
1387 /// # use futures::StreamExt;
1388 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1389 /// let keyed_singleton = // { 1: 2, 2: 4 }
1390 /// # process
1391 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1392 /// # .into_keyed()
1393 /// # .first();
1394 /// keyed_singleton.keys()
1395 /// # }, |mut stream| async move {
1396 /// // 1, 2 in any order
1397 /// # let mut results = Vec::new();
1398 /// # for _ in 0..2 {
1399 /// # results.push(stream.next().await.unwrap());
1400 /// # }
1401 /// # results.sort();
1402 /// # assert_eq!(results, vec![1, 2]);
1403 /// # }));
1404 /// # }
1405 /// ```
1406 pub fn keys(self) -> Stream<K, L, B::UnderlyingBound, NoOrder, ExactlyOnce> {
1407 self.entries().map(q!(|(k, _)| k))
1408 }
1409
1410 /// Given a bounded stream of keys `K`, returns a new keyed singleton containing only the
1411 /// entries whose keys are not in the provided stream.
1412 ///
1413 /// # Example
1414 /// ```rust
1415 /// # #[cfg(feature = "deploy")] {
1416 /// # use hydro_lang::prelude::*;
1417 /// # use futures::StreamExt;
1418 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1419 /// let tick = process.tick();
1420 /// let keyed_singleton = // { 1: 2, 2: 4 }
1421 /// # process
1422 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1423 /// # .into_keyed()
1424 /// # .first()
1425 /// # .batch(&tick, nondet!(/** test */));
1426 /// let keys_to_remove = process
1427 /// .source_iter(q!(vec![1]))
1428 /// .batch(&tick, nondet!(/** test */));
1429 /// keyed_singleton.filter_key_not_in(keys_to_remove)
1430 /// # .entries().all_ticks()
1431 /// # }, |mut stream| async move {
1432 /// // { 2: 4 }
1433 /// # for w in vec![(2, 4)] {
1434 /// # assert_eq!(stream.next().await.unwrap(), w);
1435 /// # }
1436 /// # }));
1437 /// # }
1438 /// ```
1439 pub fn filter_key_not_in<O2: Ordering, R2: Retries>(
1440 self,
1441 other: Stream<K, L, Bounded, O2, R2>,
1442 ) -> Self
1443 where
1444 K: Hash + Eq,
1445 {
1446 check_matching_location(&self.location, &other.location);
1447
1448 KeyedSingleton::new(
1449 self.location.clone(),
1450 HydroNode::AntiJoin {
1451 pos: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1452 neg: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
1453 metadata: self.location.new_node_metadata(Self::collection_kind()),
1454 },
1455 )
1456 }
1457
1458 /// An operator which allows you to "inspect" each value of a keyed singleton without
1459 /// modifying it. The closure `f` is called on a reference to each value. This is
1460 /// mainly useful for debugging, and should not be used to generate side-effects.
1461 ///
1462 /// # Example
1463 /// ```rust
1464 /// # #[cfg(feature = "deploy")] {
1465 /// # use hydro_lang::prelude::*;
1466 /// # use futures::StreamExt;
1467 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1468 /// let keyed_singleton = // { 1: 2, 2: 4 }
1469 /// # process
1470 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1471 /// # .into_keyed()
1472 /// # .first();
1473 /// keyed_singleton
1474 /// .inspect(q!(|v| println!("{}", v)))
1475 /// # .entries()
1476 /// # }, |mut stream| async move {
1477 /// // { 1: 2, 2: 4 }
1478 /// # for w in vec![(1, 2), (2, 4)] {
1479 /// # assert_eq!(stream.next().await.unwrap(), w);
1480 /// # }
1481 /// # }));
1482 /// # }
1483 /// ```
1484 pub fn inspect<F>(
1485 self,
1486 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>> + Copy,
1487 ) -> Self
1488 where
1489 F: Fn(&V) + 'a,
1490 {
1491 let f: ManualExpr<F, _> =
1492 ManualExpr::new(move |ctx: &OperatorContext<L, B::UnderlyingBound>| {
1493 f.splice_fn1_borrow_ctx(ctx)
1494 });
1495 let inspect_f = q!({
1496 let orig = f;
1497 move |t: &(_, _)| orig(&t.1)
1498 })
1499 .splice_fn1_borrow_ctx::<(K, V), ()>(&OperatorContext::<L, B::UnderlyingBound>::new(
1500 &self.location,
1501 ))
1502 .into();
1503
1504 KeyedSingleton::new(
1505 self.location.clone(),
1506 HydroNode::Inspect {
1507 f: inspect_f,
1508 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1509 metadata: self.location.new_node_metadata(Self::collection_kind()),
1510 },
1511 )
1512 }
1513
1514 /// An operator which allows you to "inspect" each entry of a keyed singleton without
1515 /// modifying it. The closure `f` is called on a reference to each key-value pair. This is
1516 /// mainly useful for debugging, and should not be used to generate side-effects.
1517 ///
1518 /// # Example
1519 /// ```rust
1520 /// # #[cfg(feature = "deploy")] {
1521 /// # use hydro_lang::prelude::*;
1522 /// # use futures::StreamExt;
1523 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1524 /// let keyed_singleton = // { 1: 2, 2: 4 }
1525 /// # process
1526 /// # .source_iter(q!(vec![(1, 2), (2, 4)]))
1527 /// # .into_keyed()
1528 /// # .first();
1529 /// keyed_singleton
1530 /// .inspect_with_key(q!(|(k, v)| println!("{}: {}", k, v)))
1531 /// # .entries()
1532 /// # }, |mut stream| async move {
1533 /// // { 1: 2, 2: 4 }
1534 /// # for w in vec![(1, 2), (2, 4)] {
1535 /// # assert_eq!(stream.next().await.unwrap(), w);
1536 /// # }
1537 /// # }));
1538 /// # }
1539 /// ```
1540 pub fn inspect_with_key<F>(
1541 self,
1542 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>>,
1543 ) -> Self
1544 where
1545 F: Fn(&(K, V)) + 'a,
1546 {
1547 let inspect_f = f
1548 .splice_fn1_borrow_ctx::<(K, V), ()>(&OperatorContext::<L, B::UnderlyingBound>::new(
1549 &self.location,
1550 ))
1551 .into();
1552
1553 KeyedSingleton::new(
1554 self.location.clone(),
1555 HydroNode::Inspect {
1556 f: inspect_f,
1557 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1558 metadata: self.location.new_node_metadata(Self::collection_kind()),
1559 },
1560 )
1561 }
1562
1563 /// Gets the key-value tuple with the largest key among all entries in this [`KeyedSingleton`].
1564 ///
1565 /// Because this method requires values to be bounded, the output [`Optional`] will only be
1566 /// asynchronously updated if a new key is added that is higher than the previous max key.
1567 ///
1568 /// # Example
1569 /// ```rust
1570 /// # #[cfg(feature = "deploy")] {
1571 /// # use hydro_lang::prelude::*;
1572 /// # use futures::StreamExt;
1573 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1574 /// let tick = process.tick();
1575 /// let keyed_singleton = // { 1: 123, 2: 456, 0: 789 }
1576 /// # Stream::<_, _>::from(process.source_iter(q!(vec![(1, 123), (2, 456), (0, 789)])))
1577 /// # .into_keyed()
1578 /// # .first();
1579 /// keyed_singleton.get_max_key()
1580 /// # .sample_eager(nondet!(/** test */))
1581 /// # }, |mut stream| async move {
1582 /// // (2, 456)
1583 /// # assert_eq!(stream.next().await.unwrap(), (2, 456));
1584 /// # }));
1585 /// # }
1586 /// ```
1587 pub fn get_max_key(
1588 self,
1589 ) -> Optional<(K, V), L, <B::UnderlyingBound as Boundedness>::AggregatedOptional>
1590 where
1591 K: Ord,
1592 {
1593 self.entries()
1594 .assume_ordering_trusted(nondet!(
1595 /// There is only one element associated with each key, and the keys are totallly
1596 /// ordered so we will produce a deterministic value. The closure technically
1597 /// isn't commutative in the case where both passed entries have the same key
1598 /// but different values.
1599 ///
1600 /// In the future, we may want to have an `assume!(...)` statement in the UDF that
1601 /// the two inputs do not have the same key.
1602 ))
1603 .reduce(q!(
1604 move |curr, new| {
1605 if new.0 > curr.0 {
1606 *curr = new;
1607 }
1608 },
1609 idempotent = manual_proof!(/** repeated elements are ignored */)
1610 ))
1611 }
1612
1613 /// Converts this keyed singleton into a [`KeyedStream`] with each group having a single
1614 /// element, the value.
1615 ///
1616 /// This is the equivalent of [`Singleton::into_stream`] but keyed.
1617 ///
1618 /// # Example
1619 /// ```rust
1620 /// # #[cfg(feature = "deploy")] {
1621 /// # use hydro_lang::prelude::*;
1622 /// # use futures::StreamExt;
1623 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1624 /// let keyed_singleton = // { 1: 2, 2: 4 }
1625 /// # Stream::<_, _>::from(process.source_iter(q!(vec![(1, 2), (2, 4)])))
1626 /// # .into_keyed()
1627 /// # .first();
1628 /// keyed_singleton
1629 /// .clone()
1630 /// .into_keyed_stream()
1631 /// .merge_unordered(
1632 /// keyed_singleton.into_keyed_stream()
1633 /// )
1634 /// # .entries()
1635 /// # }, |mut stream| async move {
1636 /// /// // { 1: [2, 2], 2: [4, 4] }
1637 /// # for w in vec![(1, 2), (2, 4), (1, 2), (2, 4)] {
1638 /// # assert_eq!(stream.next().await.unwrap(), w);
1639 /// # }
1640 /// # }));
1641 /// # }
1642 /// ```
1643 pub fn into_keyed_stream(
1644 self,
1645 ) -> KeyedStream<K, V, L, B::UnderlyingBound, TotalOrder, ExactlyOnce> {
1646 KeyedStream::new(
1647 self.location.clone(),
1648 HydroNode::Cast {
1649 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1650 metadata: self.location.new_node_metadata(KeyedStream::<
1651 K,
1652 V,
1653 L,
1654 B::UnderlyingBound,
1655 TotalOrder,
1656 ExactlyOnce,
1657 >::collection_kind()),
1658 },
1659 )
1660 }
1661}
1662
1663impl<'a, K, V, L, B: KeyedSingletonBound> KeyedSingleton<K, V, L, B>
1664where
1665 L: Location<'a>,
1666 B: KeyedSingletonBound<ValueBound = Bounded>,
1667{
1668 /// Shifts this bounded-value keyed singleton into an atomic context, which guarantees that any downstream logic
1669 /// will all be executed synchronously before any outputs are yielded (in [`KeyedSingleton::end_atomic`]).
1670 ///
1671 /// This is useful to enforce local consistency constraints, such as ensuring that a write is
1672 /// processed before an acknowledgement is emitted.
1673 pub fn atomic(self) -> KeyedSingleton<K, V, Atomic<L>, B>
1674 where
1675 L: TopLevel<'a>,
1676 {
1677 let out_location = Atomic {
1678 tick: self.location.tick(),
1679 };
1680 KeyedSingleton::new(
1681 out_location.clone(),
1682 HydroNode::BeginAtomic {
1683 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1684 metadata: out_location
1685 .new_node_metadata(KeyedSingleton::<K, V, Atomic<L>, B>::collection_kind()),
1686 },
1687 )
1688 }
1689}
1690
1691impl<'a, K, V, L, B: KeyedSingletonBound> KeyedSingleton<K, V, Atomic<L>, B>
1692where
1693 L: Location<'a>,
1694{
1695 /// Yields the elements of this keyed singleton back into a top-level, asynchronous execution context.
1696 /// See [`KeyedSingleton::atomic`] for more details.
1697 pub fn end_atomic(self) -> KeyedSingleton<K, V, L, B> {
1698 KeyedSingleton::new(
1699 self.location.tick.l.clone(),
1700 HydroNode::EndAtomic {
1701 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1702 metadata: self
1703 .location
1704 .tick
1705 .l
1706 .new_node_metadata(KeyedSingleton::<K, V, L, B>::collection_kind()),
1707 },
1708 )
1709 }
1710}
1711
1712impl<'a, K, V, L: Location<'a>> KeyedSingleton<K, V, Tick<L>, Bounded> {
1713 /// Shifts the state in `self` to the **next tick**, so that the returned keyed singleton at
1714 /// tick `T` always has the entries of `self` at tick `T - 1`.
1715 ///
1716 /// At tick `0`, the output has no entries, since there is no previous tick.
1717 ///
1718 /// This operator enables stateful iterative processing with ticks, by sending data from one
1719 /// tick to the next. For example, you can use it to compare state across consecutive batches.
1720 ///
1721 /// # Example
1722 /// ```rust
1723 /// # #[cfg(feature = "deploy")] {
1724 /// # use hydro_lang::prelude::*;
1725 /// # use futures::StreamExt;
1726 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1727 /// let tick = process.tick();
1728 /// # // ticks are lazy by default, forces the second tick to run
1729 /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1730 /// # let batch_first_tick = process
1731 /// # .source_iter(q!(vec![(1, 2), (2, 3)]))
1732 /// # .batch(&tick, nondet!(/** test */))
1733 /// # .into_keyed();
1734 /// # let batch_second_tick = process
1735 /// # .source_iter(q!(vec![(2, 4), (3, 5)]))
1736 /// # .batch(&tick, nondet!(/** test */))
1737 /// # .into_keyed()
1738 /// # .defer_tick(); // appears on the second tick
1739 /// let input_batch = // first tick: { 1: 2, 2: 3 }, second tick: { 2: 4, 3: 5 }
1740 /// # batch_first_tick.chain(batch_second_tick).first();
1741 /// input_batch.clone().filter_key_not_in(
1742 /// input_batch.defer_tick().keys() // keys present in the previous tick
1743 /// )
1744 /// # .entries().all_ticks()
1745 /// # }, |mut stream| async move {
1746 /// // { 1: 2, 2: 3 } (first tick), { 3: 5 } (second tick)
1747 /// # for w in vec![(1, 2), (2, 3), (3, 5)] {
1748 /// # assert_eq!(stream.next().await.unwrap(), w);
1749 /// # }
1750 /// # }));
1751 /// # }
1752 /// ```
1753 pub fn defer_tick(self) -> KeyedSingleton<K, V, Tick<L>, Bounded> {
1754 KeyedSingleton::new(
1755 self.location.clone(),
1756 HydroNode::DeferTick {
1757 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1758 metadata: self
1759 .location
1760 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1761 },
1762 )
1763 }
1764}
1765
1766impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Unbounded>> KeyedSingleton<K, V, L, B>
1767where
1768 L: Location<'a>,
1769{
1770 /// Returns a keyed singleton with a snapshot of each key-value entry at a non-deterministic
1771 /// point in time.
1772 ///
1773 /// # Non-Determinism
1774 /// Because this picks a snapshot of each entry, which is continuously changing, each output has a
1775 /// non-deterministic set of entries since each snapshot can be at an arbitrary point in time.
1776 ///
1777 /// In simulation tests, the snapshot decisions can be scripted by attaching a
1778 /// [`KeyedSnapshotHook`](crate::sim_hooks::KeyedSnapshotHook) to the guard via
1779 /// `nondet!(/** reason */ hook = my_hook)`.
1780 pub fn snapshot<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1781 self,
1782 tick: &Tick<L2>,
1783 mut nondet: NonDet<Option<crate::sim_hooks::KeyedSnapshotHook<K, V>>>,
1784 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
1785 assert_eq!(
1786 Location::id(tick.parent_location()),
1787 Location::id(&self.location)
1788 );
1789 let mut metadata =
1790 tick.new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind());
1791 metadata.op.sim_hook_id = nondet.take_hook().map(|h| h.id);
1792 KeyedSingleton::new(
1793 tick.drop_consistency(),
1794 HydroNode::Batch {
1795 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1796 metadata,
1797 },
1798 )
1799 }
1800}
1801
1802impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Unbounded>> KeyedSingleton<K, V, Atomic<L>, B>
1803where
1804 L: Location<'a>,
1805{
1806 /// Returns a keyed singleton with a snapshot of each key-value entry, consistent with the
1807 /// state of the keyed singleton being atomically processed.
1808 ///
1809 /// # Non-Determinism
1810 /// Because this picks a snapshot of each entry, which is continuously changing, each output has a
1811 /// non-deterministic set of entries since each snapshot can be at an arbitrary point in time.
1812 pub fn snapshot_atomic<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1813 self,
1814 tick: &Tick<L2>,
1815 _nondet: NonDet,
1816 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
1817 assert_eq!(
1818 Location::id(tick.parent_location()),
1819 Location::id(self.location.tick.parent_location())
1820 );
1821 KeyedSingleton::new(
1822 tick.drop_consistency(),
1823 HydroNode::Batch {
1824 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1825 metadata: tick
1826 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1827 },
1828 )
1829 }
1830}
1831
1832impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Bounded>> KeyedSingleton<K, V, L, B>
1833where
1834 L: Location<'a>,
1835{
1836 /// Creates a keyed singleton containing only the key-value pairs where the value satisfies a predicate `f`.
1837 ///
1838 /// The closure `f` receives a reference `&V` to each value and returns a boolean. If the predicate
1839 /// returns `true`, the key-value pair is included in the output. If it returns `false`, the pair
1840 /// is filtered out.
1841 ///
1842 /// The closure `f` receives a reference `&V` rather than an owned value `V` because filtering does
1843 /// not modify or take ownership of the values. If you need to modify the values while filtering
1844 /// use [`KeyedSingleton::filter_map`] instead.
1845 ///
1846 /// # Example
1847 /// ```rust
1848 /// # #[cfg(feature = "deploy")] {
1849 /// # use hydro_lang::prelude::*;
1850 /// # use futures::StreamExt;
1851 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1852 /// let keyed_singleton = // { 1: 2, 2: 4, 3: 1 }
1853 /// # process
1854 /// # .source_iter(q!(vec![(1, 2), (2, 4), (3, 1)]))
1855 /// # .into_keyed()
1856 /// # .first();
1857 /// keyed_singleton.filter(q!(|&v| v > 1))
1858 /// # .entries()
1859 /// # }, |mut stream| async move {
1860 /// // { 1: 2, 2: 4 }
1861 /// # let mut results = Vec::new();
1862 /// # for _ in 0..2 {
1863 /// # results.push(stream.next().await.unwrap());
1864 /// # }
1865 /// # results.sort();
1866 /// # assert_eq!(results, vec![(1, 2), (2, 4)]);
1867 /// # }));
1868 /// # }
1869 /// ```
1870 pub fn filter<F>(
1871 self,
1872 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>> + Copy,
1873 ) -> KeyedSingleton<K, V, L, B>
1874 where
1875 F: Fn(&V) -> bool + 'a,
1876 {
1877 let f: ManualExpr<F, _> =
1878 ManualExpr::new(move |ctx: &OperatorContext<L, B::UnderlyingBound>| {
1879 f.splice_fn1_borrow_ctx(ctx)
1880 });
1881 let filter_f = q!({
1882 let orig = f;
1883 move |t: &(_, _)| orig(&t.1)
1884 })
1885 .splice_fn1_borrow_ctx::<(K, V), bool>(&OperatorContext::<L, B::UnderlyingBound>::new(
1886 &self.location,
1887 ))
1888 .into();
1889
1890 KeyedSingleton::new(
1891 self.location.clone(),
1892 HydroNode::Filter {
1893 f: filter_f,
1894 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1895 metadata: self
1896 .location
1897 .new_node_metadata(KeyedSingleton::<K, V, L, B>::collection_kind()),
1898 },
1899 )
1900 }
1901
1902 /// An operator that both filters and maps values. It yields only the key-value pairs where
1903 /// the supplied closure `f` returns `Some(value)`.
1904 ///
1905 /// The closure `f` receives each value `V` and returns `Option<U>`. If the closure returns
1906 /// `Some(new_value)`, the key-value pair `(key, new_value)` is included in the output.
1907 /// If it returns `None`, the key-value pair is filtered out.
1908 ///
1909 /// # Example
1910 /// ```rust
1911 /// # #[cfg(feature = "deploy")] {
1912 /// # use hydro_lang::prelude::*;
1913 /// # use futures::StreamExt;
1914 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1915 /// let keyed_singleton = // { 1: "42", 2: "hello", 3: "100" }
1916 /// # process
1917 /// # .source_iter(q!(vec![(1, "42"), (2, "hello"), (3, "100")]))
1918 /// # .into_keyed()
1919 /// # .first();
1920 /// keyed_singleton.filter_map(q!(|s| s.parse::<i32>().ok()))
1921 /// # .entries()
1922 /// # }, |mut stream| async move {
1923 /// // { 1: 42, 3: 100 }
1924 /// # let mut results = Vec::new();
1925 /// # for _ in 0..2 {
1926 /// # results.push(stream.next().await.unwrap());
1927 /// # }
1928 /// # results.sort();
1929 /// # assert_eq!(results, vec![(1, 42), (3, 100)]);
1930 /// # }));
1931 /// # }
1932 /// ```
1933 pub fn filter_map<F, U>(
1934 self,
1935 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>> + Copy,
1936 ) -> KeyedSingleton<K, U, L, B::EraseMonotonic>
1937 where
1938 F: Fn(V) -> Option<U> + 'a,
1939 {
1940 let f: ManualExpr<F, _> =
1941 ManualExpr::new(move |ctx: &OperatorContext<L, B::UnderlyingBound>| {
1942 f.splice_fn1_ctx(ctx)
1943 });
1944 let filter_map_f = q!({
1945 let orig = f;
1946 move |(k, v)| orig(v).map(|o| (k, o))
1947 })
1948 .splice_fn1_ctx::<(K, V), Option<(K, U)>>(&OperatorContext::<L, B::UnderlyingBound>::new(
1949 &self.location,
1950 ))
1951 .into();
1952
1953 KeyedSingleton::new(
1954 self.location.clone(),
1955 HydroNode::FilterMap {
1956 f: filter_map_f,
1957 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1958 metadata: self.location.new_node_metadata(KeyedSingleton::<
1959 K,
1960 U,
1961 L,
1962 B::EraseMonotonic,
1963 >::collection_kind()),
1964 },
1965 )
1966 }
1967
1968 /// Returns a keyed singleton with entries consisting of _new_ key-value pairs that have
1969 /// arrived since the previous batch was released.
1970 ///
1971 /// Currently, there is no `all_ticks` dual on [`KeyedSingleton`], instead you may want to use
1972 /// [`KeyedSingleton::into_keyed_stream`] then yield with [`KeyedStream::all_ticks`].
1973 ///
1974 /// # Non-Determinism
1975 /// Because this picks a batch of asynchronously added entries, each output keyed singleton
1976 /// has a non-deterministic set of key-value pairs.
1977 ///
1978 /// In simulation tests, the batching decisions can be scripted by attaching a
1979 /// [`KeyedSnapshotHook`](crate::sim_hooks::KeyedSnapshotHook) to the guard via
1980 /// `nondet!(/** reason */ hook = my_hook)`.
1981 pub fn batch<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1982 self,
1983 tick: &Tick<L2>,
1984 mut nondet: NonDet<Option<crate::sim_hooks::KeyedSnapshotHook<K, V>>>,
1985 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
1986 assert_eq!(
1987 Location::id(tick.parent_location()),
1988 Location::id(&self.location)
1989 );
1990 let mut metadata =
1991 tick.new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind());
1992 metadata.op.sim_hook_id = nondet.take_hook().map(|h| h.id);
1993 KeyedSingleton::new(
1994 tick.drop_consistency(),
1995 HydroNode::Batch {
1996 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1997 metadata,
1998 },
1999 )
2000 }
2001}
2002
2003impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Bounded>> KeyedSingleton<K, V, Atomic<L>, B>
2004where
2005 L: Location<'a>,
2006{
2007 /// Returns a keyed singleton with entries consisting of _new_ key-value pairs that are being
2008 /// atomically processed.
2009 ///
2010 /// Currently, there is no dual to asynchronously yield back outside the tick, instead you
2011 /// should use [`KeyedSingleton::into_keyed_stream`] and yield a [`KeyedStream`].
2012 ///
2013 /// # Non-Determinism
2014 /// Because this picks a batch of asynchronously added entries, each output keyed singleton
2015 /// has a non-deterministic set of key-value pairs.
2016 pub fn batch_atomic<L2: Location<'a, DropConsistency = L::DropConsistency>>(
2017 self,
2018 tick: &Tick<L2>,
2019 nondet: NonDet,
2020 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
2021 let _ = nondet;
2022 assert_eq!(
2023 Location::id(tick.parent_location()),
2024 Location::id(self.location.tick.parent_location())
2025 );
2026 KeyedSingleton::new(
2027 tick.drop_consistency(),
2028 HydroNode::Batch {
2029 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2030 metadata: tick
2031 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
2032 },
2033 )
2034 }
2035}
2036
2037#[cfg(test)]
2038mod tests {
2039 #[cfg(feature = "deploy")]
2040 use futures::{SinkExt, StreamExt};
2041 #[cfg(feature = "deploy")]
2042 use hydro_deploy::Deployment;
2043 #[cfg(any(feature = "deploy", feature = "sim"))]
2044 use stageleft::q;
2045
2046 #[cfg(any(feature = "deploy", feature = "sim"))]
2047 use crate::compile::builder::FlowBuilder;
2048 #[cfg(any(feature = "deploy", feature = "sim"))]
2049 use crate::location::Location;
2050 #[cfg(any(feature = "deploy", feature = "sim"))]
2051 use crate::nondet::nondet;
2052
2053 #[cfg(feature = "deploy")]
2054 #[tokio::test]
2055 async fn key_count_bounded_value() {
2056 let mut deployment = Deployment::new();
2057
2058 let mut flow = FlowBuilder::new();
2059 let node = flow.process::<()>();
2060 let external = flow.external::<()>();
2061
2062 let (input_port, input) = node.source_external_bincode(&external);
2063 let out = input
2064 .into_keyed()
2065 .first()
2066 .key_count()
2067 .sample_eager(nondet!(/** test */))
2068 .send_bincode_external(&external);
2069
2070 let nodes = flow
2071 .with_process(&node, deployment.Localhost())
2072 .with_external(&external, deployment.Localhost())
2073 .deploy(&mut deployment);
2074
2075 deployment.deploy().await.unwrap();
2076
2077 let mut external_in = nodes.connect(input_port).await;
2078 let mut external_out = nodes.connect(out).await;
2079
2080 deployment.start().await.unwrap();
2081
2082 assert_eq!(external_out.next().await.unwrap(), 0);
2083
2084 external_in.send((1, 1)).await.unwrap();
2085 assert_eq!(external_out.next().await.unwrap(), 1);
2086
2087 external_in.send((2, 2)).await.unwrap();
2088 assert_eq!(external_out.next().await.unwrap(), 2);
2089 }
2090
2091 #[cfg(feature = "deploy")]
2092 #[tokio::test]
2093 async fn key_count_unbounded_value() {
2094 let mut deployment = Deployment::new();
2095
2096 let mut flow = FlowBuilder::new();
2097 let node = flow.process::<()>();
2098 let external = flow.external::<()>();
2099
2100 let (input_port, input) = node.source_external_bincode(&external);
2101 let out = input
2102 .into_keyed()
2103 .fold(q!(|| 0), q!(|acc, _| *acc += 1))
2104 .key_count()
2105 .sample_eager(nondet!(/** test */))
2106 .send_bincode_external(&external);
2107
2108 let nodes = flow
2109 .with_process(&node, deployment.Localhost())
2110 .with_external(&external, deployment.Localhost())
2111 .deploy(&mut deployment);
2112
2113 deployment.deploy().await.unwrap();
2114
2115 let mut external_in = nodes.connect(input_port).await;
2116 let mut external_out = nodes.connect(out).await;
2117
2118 deployment.start().await.unwrap();
2119
2120 assert_eq!(external_out.next().await.unwrap(), 0);
2121
2122 external_in.send((1, 1)).await.unwrap();
2123 assert_eq!(external_out.next().await.unwrap(), 1);
2124
2125 external_in.send((1, 2)).await.unwrap();
2126 assert_eq!(external_out.next().await.unwrap(), 1);
2127
2128 external_in.send((2, 2)).await.unwrap();
2129 assert_eq!(external_out.next().await.unwrap(), 2);
2130
2131 external_in.send((1, 1)).await.unwrap();
2132 assert_eq!(external_out.next().await.unwrap(), 2);
2133
2134 external_in.send((3, 1)).await.unwrap();
2135 assert_eq!(external_out.next().await.unwrap(), 3);
2136 }
2137
2138 #[cfg(feature = "deploy")]
2139 #[tokio::test]
2140 async fn into_singleton_bounded_value() {
2141 let mut deployment = Deployment::new();
2142
2143 let mut flow = FlowBuilder::new();
2144 let node = flow.process::<()>();
2145 let external = flow.external::<()>();
2146
2147 let (input_port, input) = node.source_external_bincode(&external);
2148 let out = input
2149 .into_keyed()
2150 .first()
2151 .into_singleton()
2152 .sample_eager(nondet!(/** test */))
2153 .send_bincode_external(&external);
2154
2155 let nodes = flow
2156 .with_process(&node, deployment.Localhost())
2157 .with_external(&external, deployment.Localhost())
2158 .deploy(&mut deployment);
2159
2160 deployment.deploy().await.unwrap();
2161
2162 let mut external_in = nodes.connect(input_port).await;
2163 let mut external_out = nodes.connect(out).await;
2164
2165 deployment.start().await.unwrap();
2166
2167 assert_eq!(
2168 external_out.next().await.unwrap(),
2169 std::collections::HashMap::new()
2170 );
2171
2172 external_in.send((1, 1)).await.unwrap();
2173 assert_eq!(
2174 external_out.next().await.unwrap(),
2175 vec![(1, 1)].into_iter().collect()
2176 );
2177
2178 external_in.send((2, 2)).await.unwrap();
2179 assert_eq!(
2180 external_out.next().await.unwrap(),
2181 vec![(1, 1), (2, 2)].into_iter().collect()
2182 );
2183 }
2184
2185 #[cfg(feature = "deploy")]
2186 #[tokio::test]
2187 async fn into_singleton_unbounded_value() {
2188 let mut deployment = Deployment::new();
2189
2190 let mut flow = FlowBuilder::new();
2191 let node = flow.process::<()>();
2192 let external = flow.external::<()>();
2193
2194 let (input_port, input) = node.source_external_bincode(&external);
2195 let out = input
2196 .into_keyed()
2197 .fold(q!(|| 0), q!(|acc, _| *acc += 1))
2198 .into_singleton()
2199 .sample_eager(nondet!(/** test */))
2200 .send_bincode_external(&external);
2201
2202 let nodes = flow
2203 .with_process(&node, deployment.Localhost())
2204 .with_external(&external, deployment.Localhost())
2205 .deploy(&mut deployment);
2206
2207 deployment.deploy().await.unwrap();
2208
2209 let mut external_in = nodes.connect(input_port).await;
2210 let mut external_out = nodes.connect(out).await;
2211
2212 deployment.start().await.unwrap();
2213
2214 assert_eq!(
2215 external_out.next().await.unwrap(),
2216 std::collections::HashMap::new()
2217 );
2218
2219 external_in.send((1, 1)).await.unwrap();
2220 assert_eq!(
2221 external_out.next().await.unwrap(),
2222 vec![(1, 1)].into_iter().collect()
2223 );
2224
2225 external_in.send((1, 2)).await.unwrap();
2226 assert_eq!(
2227 external_out.next().await.unwrap(),
2228 vec![(1, 2)].into_iter().collect()
2229 );
2230
2231 external_in.send((2, 2)).await.unwrap();
2232 assert_eq!(
2233 external_out.next().await.unwrap(),
2234 vec![(1, 2), (2, 1)].into_iter().collect()
2235 );
2236
2237 external_in.send((1, 1)).await.unwrap();
2238 assert_eq!(
2239 external_out.next().await.unwrap(),
2240 vec![(1, 3), (2, 1)].into_iter().collect()
2241 );
2242
2243 external_in.send((3, 1)).await.unwrap();
2244 assert_eq!(
2245 external_out.next().await.unwrap(),
2246 vec![(1, 3), (2, 1), (3, 1)].into_iter().collect()
2247 );
2248 }
2249
2250 #[cfg(feature = "sim")]
2251 #[test]
2252 fn sim_unbounded_singleton_snapshot() {
2253 let mut flow = FlowBuilder::new();
2254 let node = flow.process::<()>();
2255
2256 let (input_port, input) = node.sim_input();
2257 let output = input
2258 .into_keyed()
2259 .fold(q!(|| 0), q!(|acc, _| *acc += 1))
2260 .snapshot(&node.tick(), nondet!(/** test */))
2261 .entries()
2262 .all_ticks()
2263 .sim_output();
2264
2265 let count = flow.sim().exhaustive(async || {
2266 input_port.send((1, 123));
2267 input_port.send((1, 456));
2268 input_port.send((2, 123));
2269
2270 let all = output.collect_sorted::<Vec<_>>().await;
2271 assert_eq!(all.last().unwrap(), &(2, 1));
2272 });
2273
2274 assert_eq!(count, 8);
2275 }
2276
2277 #[cfg(feature = "deploy")]
2278 #[tokio::test]
2279 async fn join_keyed_stream() {
2280 let mut deployment = Deployment::new();
2281
2282 let mut flow = FlowBuilder::new();
2283 let node = flow.process::<()>();
2284 let external = flow.external::<()>();
2285
2286 let tick = node.tick();
2287 let keyed_data = node
2288 .source_iter(q!(vec![(1, 10), (2, 20)]))
2289 .into_keyed()
2290 .batch(&tick, nondet!(/** test */))
2291 .first();
2292 let requests = node
2293 .source_iter(q!(vec![(1, 100), (2, 200), (3, 300)]))
2294 .into_keyed()
2295 .batch(&tick, nondet!(/** test */));
2296
2297 let out = keyed_data
2298 .join_keyed_stream(requests)
2299 .entries()
2300 .all_ticks()
2301 .send_bincode_external(&external);
2302
2303 let nodes = flow
2304 .with_process(&node, deployment.Localhost())
2305 .with_external(&external, deployment.Localhost())
2306 .deploy(&mut deployment);
2307
2308 deployment.deploy().await.unwrap();
2309
2310 let mut external_out = nodes.connect(out).await;
2311
2312 deployment.start().await.unwrap();
2313
2314 let mut results = vec![];
2315 for _ in 0..2 {
2316 results.push(external_out.next().await.unwrap());
2317 }
2318 results.sort();
2319
2320 assert_eq!(results, vec![(1, (10, 100)), (2, (20, 200))]);
2321 }
2322
2323 #[cfg(feature = "sim")]
2324 #[test]
2325 fn threshold_greater_or_equal_monotonic() {
2326 let mut flow = FlowBuilder::new();
2327 let node = flow.process::<()>();
2328
2329 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2330 let (thresh_port, thresh_input) = node.sim_input::<(u32, usize), _, _>();
2331
2332 // Create a monotonically increasing keyed singleton via fold with monotone proof
2333 let counts: super::KeyedSingleton<u32, usize, _, super::MonotonicValue> =
2334 input.into_keyed().fold(
2335 q!(|| 0usize),
2336 q!(
2337 |acc, v| *acc += v,
2338 monotone = crate::properties::manual_proof!(/** += is monotonic */)
2339 ),
2340 );
2341
2342 // BoundedValue keyed singleton of thresholds (from .first() on unbounded stream)
2343 let thresholds = thresh_input.into_keyed().first();
2344
2345 let output = counts
2346 .threshold_greater_or_equal(thresholds)
2347 .entries()
2348 .sim_output();
2349
2350 let count = flow.sim().exhaustive(async || {
2351 // Set thresholds: key 1 needs value >= 5, key 2 needs value >= 10
2352 thresh_port.send((1, 5));
2353 thresh_port.send((2, 10));
2354
2355 // key 1 gets increments: 3 + 3 = 6, which is >= 5 ✓
2356 input_port.send((1, 3));
2357 input_port.send((1, 3));
2358 // key 2 gets increments: 3 + 3 = 6, which is < 10 ✗
2359 input_port.send((2, 3));
2360 input_port.send((2, 3));
2361
2362 let results = output.collect_sorted::<Vec<_>>().await;
2363 assert_eq!(results, vec![(1, 5)]);
2364 });
2365
2366 assert!(count > 0);
2367 }
2368
2369 #[cfg(feature = "sim")]
2370 #[test]
2371 fn threshold_greater_or_equal_uniform() {
2372 let mut flow = FlowBuilder::new();
2373 let node = flow.process::<()>();
2374
2375 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2376
2377 let counts: super::KeyedSingleton<u32, usize, _, super::MonotonicValue> =
2378 input.into_keyed().fold(
2379 q!(|| 0usize),
2380 q!(
2381 |acc, v| *acc += v,
2382 monotone = crate::properties::manual_proof!(/** += is monotonic */)
2383 ),
2384 );
2385
2386 // Uniform threshold: all keys need value >= 5
2387 let threshold = node.singleton(q!(5usize));
2388
2389 let output = counts
2390 .threshold_greater_or_equal_uniform(threshold)
2391 .entries()
2392 .sim_output();
2393
2394 let count = flow.sim().exhaustive(async || {
2395 // key 1: 3 + 3 = 6 >= 5 ✓
2396 input_port.send((1, 3));
2397 input_port.send((1, 3));
2398 // key 2: 2 + 2 = 4 < 5 ✗
2399 input_port.send((2, 2));
2400 input_port.send((2, 2));
2401
2402 let results = output.collect_sorted::<Vec<_>>().await;
2403 assert_eq!(results, vec![(1, 5)]);
2404 });
2405
2406 assert!(count > 0);
2407 }
2408
2409 #[cfg(feature = "sim")]
2410 #[test]
2411 fn threshold_greater_or_equal_bounded_value() {
2412 let mut flow = FlowBuilder::new();
2413 let node = flow.process::<()>();
2414
2415 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2416 let (thresh_port, thresh_input) = node.sim_input::<(u32, usize), _, _>();
2417
2418 // BoundedValue keyed singleton (values fixed once per key via .first())
2419 let values = input.into_keyed().first();
2420
2421 // BoundedValue keyed singleton of thresholds
2422 let thresholds = thresh_input.into_keyed().first();
2423
2424 let output = values
2425 .threshold_greater_or_equal(thresholds)
2426 .entries()
2427 .sim_output();
2428
2429 let count = flow.sim().exhaustive(async || {
2430 // Set thresholds: key 1 needs >= 3, key 2 needs >= 10
2431 thresh_port.send((1, 3));
2432 thresh_port.send((2, 10));
2433
2434 // key 1 gets value 5 >= 3 ✓, key 2 gets value 4 < 10 ✗
2435 input_port.send((1, 5));
2436 input_port.send((2, 4));
2437
2438 let results = output.collect_sorted::<Vec<_>>().await;
2439 assert_eq!(results, vec![(1, 3)]);
2440 });
2441
2442 assert!(count > 0);
2443 }
2444
2445 #[cfg(feature = "sim")]
2446 #[test]
2447 fn threshold_greater_or_equal_uniform_bounded_value() {
2448 let mut flow = FlowBuilder::new();
2449 let node = flow.process::<()>();
2450
2451 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2452
2453 // BoundedValue keyed singleton (values fixed once per key via .first())
2454 let values = input.into_keyed().first();
2455
2456 // Uniform threshold: all keys need value >= 5
2457 let threshold = node.singleton(q!(5usize));
2458
2459 let output = values
2460 .threshold_greater_or_equal_uniform(threshold)
2461 .entries()
2462 .sim_output();
2463
2464 let count = flow.sim().exhaustive(async || {
2465 // key 1 gets value 7 >= 5 ✓, key 2 gets value 3 < 5 ✗
2466 input_port.send((1, 7));
2467 input_port.send((2, 3));
2468
2469 let results = output.collect_sorted::<Vec<_>>().await;
2470 assert_eq!(results, vec![(1, 5)]);
2471 });
2472
2473 assert!(count > 0);
2474 }
2475
2476 #[cfg(feature = "sim")]
2477 #[test]
2478 fn threshold_greater_or_equal_bounded() {
2479 let mut flow = FlowBuilder::new();
2480 let node = flow.process::<()>();
2481
2482 // Bounded keyed singleton (fully known upfront)
2483 let values = node
2484 .source_iter(q!(vec![(1, 6usize), (2, 4usize)]))
2485 .into_keyed()
2486 .first();
2487
2488 // BoundedValue thresholds (from async source)
2489 let (thresh_port, thresh_input) = node.sim_input::<(u32, usize), _, _>();
2490 let thresholds = thresh_input.into_keyed().first();
2491
2492 let output = values
2493 .threshold_greater_or_equal(thresholds)
2494 .entries()
2495 .sim_output();
2496
2497 let count = flow.sim().exhaustive(async || {
2498 thresh_port.send((1, 5));
2499 thresh_port.send((2, 10));
2500
2501 // key 1: 6 >= 5 ✓, key 2: 4 < 10 ✗
2502 let results = output.collect_sorted::<Vec<_>>().await;
2503 assert_eq!(results, vec![(1, 5)]);
2504 });
2505
2506 assert!(count > 0);
2507 }
2508
2509 #[cfg(feature = "sim")]
2510 #[test]
2511 fn threshold_greater_or_equal_uniform_bounded() {
2512 let mut flow = FlowBuilder::new();
2513 let node = flow.process::<()>();
2514
2515 let values = node
2516 .source_iter(q!(vec![(1, 6usize), (2, 4usize)]))
2517 .into_keyed()
2518 .first();
2519 let threshold = node.singleton(q!(5usize));
2520
2521 let output = values
2522 .threshold_greater_or_equal_uniform(threshold)
2523 .entries()
2524 .sim_output();
2525
2526 let count = flow.sim().exhaustive(async || {
2527 // key 1: 6 >= 5 ✓, key 2: 4 < 5 ✗
2528 let results = output.collect_sorted::<Vec<_>>().await;
2529 assert_eq!(results, vec![(1, 5)]);
2530 });
2531
2532 assert!(count > 0);
2533 }
2534}