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 pub fn snapshot<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1777 self,
1778 tick: &Tick<L2>,
1779 _nondet: NonDet,
1780 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
1781 assert_eq!(
1782 Location::id(tick.parent_location()),
1783 Location::id(&self.location)
1784 );
1785 KeyedSingleton::new(
1786 tick.drop_consistency(),
1787 HydroNode::Batch {
1788 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1789 metadata: tick
1790 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1791 },
1792 )
1793 }
1794}
1795
1796impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Unbounded>> KeyedSingleton<K, V, Atomic<L>, B>
1797where
1798 L: Location<'a>,
1799{
1800 /// Returns a keyed singleton with a snapshot of each key-value entry, consistent with the
1801 /// state of the keyed singleton being atomically processed.
1802 ///
1803 /// # Non-Determinism
1804 /// Because this picks a snapshot of each entry, which is continuously changing, each output has a
1805 /// non-deterministic set of entries since each snapshot can be at an arbitrary point in time.
1806 pub fn snapshot_atomic<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1807 self,
1808 tick: &Tick<L2>,
1809 _nondet: NonDet,
1810 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
1811 assert_eq!(
1812 Location::id(tick.parent_location()),
1813 Location::id(self.location.tick.parent_location())
1814 );
1815 KeyedSingleton::new(
1816 tick.drop_consistency(),
1817 HydroNode::Batch {
1818 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1819 metadata: tick
1820 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1821 },
1822 )
1823 }
1824}
1825
1826impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Bounded>> KeyedSingleton<K, V, L, B>
1827where
1828 L: Location<'a>,
1829{
1830 /// Creates a keyed singleton containing only the key-value pairs where the value satisfies a predicate `f`.
1831 ///
1832 /// The closure `f` receives a reference `&V` to each value and returns a boolean. If the predicate
1833 /// returns `true`, the key-value pair is included in the output. If it returns `false`, the pair
1834 /// is filtered out.
1835 ///
1836 /// The closure `f` receives a reference `&V` rather than an owned value `V` because filtering does
1837 /// not modify or take ownership of the values. If you need to modify the values while filtering
1838 /// use [`KeyedSingleton::filter_map`] instead.
1839 ///
1840 /// # Example
1841 /// ```rust
1842 /// # #[cfg(feature = "deploy")] {
1843 /// # use hydro_lang::prelude::*;
1844 /// # use futures::StreamExt;
1845 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1846 /// let keyed_singleton = // { 1: 2, 2: 4, 3: 1 }
1847 /// # process
1848 /// # .source_iter(q!(vec![(1, 2), (2, 4), (3, 1)]))
1849 /// # .into_keyed()
1850 /// # .first();
1851 /// keyed_singleton.filter(q!(|&v| v > 1))
1852 /// # .entries()
1853 /// # }, |mut stream| async move {
1854 /// // { 1: 2, 2: 4 }
1855 /// # let mut results = Vec::new();
1856 /// # for _ in 0..2 {
1857 /// # results.push(stream.next().await.unwrap());
1858 /// # }
1859 /// # results.sort();
1860 /// # assert_eq!(results, vec![(1, 2), (2, 4)]);
1861 /// # }));
1862 /// # }
1863 /// ```
1864 pub fn filter<F>(
1865 self,
1866 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>> + Copy,
1867 ) -> KeyedSingleton<K, V, L, B>
1868 where
1869 F: Fn(&V) -> bool + 'a,
1870 {
1871 let f: ManualExpr<F, _> =
1872 ManualExpr::new(move |ctx: &OperatorContext<L, B::UnderlyingBound>| {
1873 f.splice_fn1_borrow_ctx(ctx)
1874 });
1875 let filter_f = q!({
1876 let orig = f;
1877 move |t: &(_, _)| orig(&t.1)
1878 })
1879 .splice_fn1_borrow_ctx::<(K, V), bool>(&OperatorContext::<L, B::UnderlyingBound>::new(
1880 &self.location,
1881 ))
1882 .into();
1883
1884 KeyedSingleton::new(
1885 self.location.clone(),
1886 HydroNode::Filter {
1887 f: filter_f,
1888 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1889 metadata: self
1890 .location
1891 .new_node_metadata(KeyedSingleton::<K, V, L, B>::collection_kind()),
1892 },
1893 )
1894 }
1895
1896 /// An operator that both filters and maps values. It yields only the key-value pairs where
1897 /// the supplied closure `f` returns `Some(value)`.
1898 ///
1899 /// The closure `f` receives each value `V` and returns `Option<U>`. If the closure returns
1900 /// `Some(new_value)`, the key-value pair `(key, new_value)` is included in the output.
1901 /// If it returns `None`, the key-value pair is filtered out.
1902 ///
1903 /// # Example
1904 /// ```rust
1905 /// # #[cfg(feature = "deploy")] {
1906 /// # use hydro_lang::prelude::*;
1907 /// # use futures::StreamExt;
1908 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1909 /// let keyed_singleton = // { 1: "42", 2: "hello", 3: "100" }
1910 /// # process
1911 /// # .source_iter(q!(vec![(1, "42"), (2, "hello"), (3, "100")]))
1912 /// # .into_keyed()
1913 /// # .first();
1914 /// keyed_singleton.filter_map(q!(|s| s.parse::<i32>().ok()))
1915 /// # .entries()
1916 /// # }, |mut stream| async move {
1917 /// // { 1: 42, 3: 100 }
1918 /// # let mut results = Vec::new();
1919 /// # for _ in 0..2 {
1920 /// # results.push(stream.next().await.unwrap());
1921 /// # }
1922 /// # results.sort();
1923 /// # assert_eq!(results, vec![(1, 42), (3, 100)]);
1924 /// # }));
1925 /// # }
1926 /// ```
1927 pub fn filter_map<F, U>(
1928 self,
1929 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>> + Copy,
1930 ) -> KeyedSingleton<K, U, L, B::EraseMonotonic>
1931 where
1932 F: Fn(V) -> Option<U> + 'a,
1933 {
1934 let f: ManualExpr<F, _> =
1935 ManualExpr::new(move |ctx: &OperatorContext<L, B::UnderlyingBound>| {
1936 f.splice_fn1_ctx(ctx)
1937 });
1938 let filter_map_f = q!({
1939 let orig = f;
1940 move |(k, v)| orig(v).map(|o| (k, o))
1941 })
1942 .splice_fn1_ctx::<(K, V), Option<(K, U)>>(&OperatorContext::<L, B::UnderlyingBound>::new(
1943 &self.location,
1944 ))
1945 .into();
1946
1947 KeyedSingleton::new(
1948 self.location.clone(),
1949 HydroNode::FilterMap {
1950 f: filter_map_f,
1951 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1952 metadata: self.location.new_node_metadata(KeyedSingleton::<
1953 K,
1954 U,
1955 L,
1956 B::EraseMonotonic,
1957 >::collection_kind()),
1958 },
1959 )
1960 }
1961
1962 /// Returns a keyed singleton with entries consisting of _new_ key-value pairs that have
1963 /// arrived since the previous batch was released.
1964 ///
1965 /// Currently, there is no `all_ticks` dual on [`KeyedSingleton`], instead you may want to use
1966 /// [`KeyedSingleton::into_keyed_stream`] then yield with [`KeyedStream::all_ticks`].
1967 ///
1968 /// # Non-Determinism
1969 /// Because this picks a batch of asynchronously added entries, each output keyed singleton
1970 /// has a non-deterministic set of key-value pairs.
1971 pub fn batch<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1972 self,
1973 tick: &Tick<L2>,
1974 _nondet: NonDet,
1975 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
1976 assert_eq!(
1977 Location::id(tick.parent_location()),
1978 Location::id(&self.location)
1979 );
1980 KeyedSingleton::new(
1981 tick.drop_consistency(),
1982 HydroNode::Batch {
1983 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1984 metadata: tick
1985 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
1986 },
1987 )
1988 }
1989}
1990
1991impl<'a, K, V, L, B: KeyedSingletonBound<ValueBound = Bounded>> KeyedSingleton<K, V, Atomic<L>, B>
1992where
1993 L: Location<'a>,
1994{
1995 /// Returns a keyed singleton with entries consisting of _new_ key-value pairs that are being
1996 /// atomically processed.
1997 ///
1998 /// Currently, there is no dual to asynchronously yield back outside the tick, instead you
1999 /// should use [`KeyedSingleton::into_keyed_stream`] and yield a [`KeyedStream`].
2000 ///
2001 /// # Non-Determinism
2002 /// Because this picks a batch of asynchronously added entries, each output keyed singleton
2003 /// has a non-deterministic set of key-value pairs.
2004 pub fn batch_atomic<L2: Location<'a, DropConsistency = L::DropConsistency>>(
2005 self,
2006 tick: &Tick<L2>,
2007 nondet: NonDet,
2008 ) -> KeyedSingleton<K, V, Tick<L::DropConsistency>, Bounded> {
2009 let _ = nondet;
2010 assert_eq!(
2011 Location::id(tick.parent_location()),
2012 Location::id(self.location.tick.parent_location())
2013 );
2014 KeyedSingleton::new(
2015 tick.drop_consistency(),
2016 HydroNode::Batch {
2017 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
2018 metadata: tick
2019 .new_node_metadata(KeyedSingleton::<K, V, Tick<L>, Bounded>::collection_kind()),
2020 },
2021 )
2022 }
2023}
2024
2025#[cfg(test)]
2026mod tests {
2027 #[cfg(feature = "deploy")]
2028 use futures::{SinkExt, StreamExt};
2029 #[cfg(feature = "deploy")]
2030 use hydro_deploy::Deployment;
2031 #[cfg(any(feature = "deploy", feature = "sim"))]
2032 use stageleft::q;
2033
2034 #[cfg(any(feature = "deploy", feature = "sim"))]
2035 use crate::compile::builder::FlowBuilder;
2036 #[cfg(any(feature = "deploy", feature = "sim"))]
2037 use crate::location::Location;
2038 #[cfg(any(feature = "deploy", feature = "sim"))]
2039 use crate::nondet::nondet;
2040
2041 #[cfg(feature = "deploy")]
2042 #[tokio::test]
2043 async fn key_count_bounded_value() {
2044 let mut deployment = Deployment::new();
2045
2046 let mut flow = FlowBuilder::new();
2047 let node = flow.process::<()>();
2048 let external = flow.external::<()>();
2049
2050 let (input_port, input) = node.source_external_bincode(&external);
2051 let out = input
2052 .into_keyed()
2053 .first()
2054 .key_count()
2055 .sample_eager(nondet!(/** test */))
2056 .send_bincode_external(&external);
2057
2058 let nodes = flow
2059 .with_process(&node, deployment.Localhost())
2060 .with_external(&external, deployment.Localhost())
2061 .deploy(&mut deployment);
2062
2063 deployment.deploy().await.unwrap();
2064
2065 let mut external_in = nodes.connect(input_port).await;
2066 let mut external_out = nodes.connect(out).await;
2067
2068 deployment.start().await.unwrap();
2069
2070 assert_eq!(external_out.next().await.unwrap(), 0);
2071
2072 external_in.send((1, 1)).await.unwrap();
2073 assert_eq!(external_out.next().await.unwrap(), 1);
2074
2075 external_in.send((2, 2)).await.unwrap();
2076 assert_eq!(external_out.next().await.unwrap(), 2);
2077 }
2078
2079 #[cfg(feature = "deploy")]
2080 #[tokio::test]
2081 async fn key_count_unbounded_value() {
2082 let mut deployment = Deployment::new();
2083
2084 let mut flow = FlowBuilder::new();
2085 let node = flow.process::<()>();
2086 let external = flow.external::<()>();
2087
2088 let (input_port, input) = node.source_external_bincode(&external);
2089 let out = input
2090 .into_keyed()
2091 .fold(q!(|| 0), q!(|acc, _| *acc += 1))
2092 .key_count()
2093 .sample_eager(nondet!(/** test */))
2094 .send_bincode_external(&external);
2095
2096 let nodes = flow
2097 .with_process(&node, deployment.Localhost())
2098 .with_external(&external, deployment.Localhost())
2099 .deploy(&mut deployment);
2100
2101 deployment.deploy().await.unwrap();
2102
2103 let mut external_in = nodes.connect(input_port).await;
2104 let mut external_out = nodes.connect(out).await;
2105
2106 deployment.start().await.unwrap();
2107
2108 assert_eq!(external_out.next().await.unwrap(), 0);
2109
2110 external_in.send((1, 1)).await.unwrap();
2111 assert_eq!(external_out.next().await.unwrap(), 1);
2112
2113 external_in.send((1, 2)).await.unwrap();
2114 assert_eq!(external_out.next().await.unwrap(), 1);
2115
2116 external_in.send((2, 2)).await.unwrap();
2117 assert_eq!(external_out.next().await.unwrap(), 2);
2118
2119 external_in.send((1, 1)).await.unwrap();
2120 assert_eq!(external_out.next().await.unwrap(), 2);
2121
2122 external_in.send((3, 1)).await.unwrap();
2123 assert_eq!(external_out.next().await.unwrap(), 3);
2124 }
2125
2126 #[cfg(feature = "deploy")]
2127 #[tokio::test]
2128 async fn into_singleton_bounded_value() {
2129 let mut deployment = Deployment::new();
2130
2131 let mut flow = FlowBuilder::new();
2132 let node = flow.process::<()>();
2133 let external = flow.external::<()>();
2134
2135 let (input_port, input) = node.source_external_bincode(&external);
2136 let out = input
2137 .into_keyed()
2138 .first()
2139 .into_singleton()
2140 .sample_eager(nondet!(/** test */))
2141 .send_bincode_external(&external);
2142
2143 let nodes = flow
2144 .with_process(&node, deployment.Localhost())
2145 .with_external(&external, deployment.Localhost())
2146 .deploy(&mut deployment);
2147
2148 deployment.deploy().await.unwrap();
2149
2150 let mut external_in = nodes.connect(input_port).await;
2151 let mut external_out = nodes.connect(out).await;
2152
2153 deployment.start().await.unwrap();
2154
2155 assert_eq!(
2156 external_out.next().await.unwrap(),
2157 std::collections::HashMap::new()
2158 );
2159
2160 external_in.send((1, 1)).await.unwrap();
2161 assert_eq!(
2162 external_out.next().await.unwrap(),
2163 vec![(1, 1)].into_iter().collect()
2164 );
2165
2166 external_in.send((2, 2)).await.unwrap();
2167 assert_eq!(
2168 external_out.next().await.unwrap(),
2169 vec![(1, 1), (2, 2)].into_iter().collect()
2170 );
2171 }
2172
2173 #[cfg(feature = "deploy")]
2174 #[tokio::test]
2175 async fn into_singleton_unbounded_value() {
2176 let mut deployment = Deployment::new();
2177
2178 let mut flow = FlowBuilder::new();
2179 let node = flow.process::<()>();
2180 let external = flow.external::<()>();
2181
2182 let (input_port, input) = node.source_external_bincode(&external);
2183 let out = input
2184 .into_keyed()
2185 .fold(q!(|| 0), q!(|acc, _| *acc += 1))
2186 .into_singleton()
2187 .sample_eager(nondet!(/** test */))
2188 .send_bincode_external(&external);
2189
2190 let nodes = flow
2191 .with_process(&node, deployment.Localhost())
2192 .with_external(&external, deployment.Localhost())
2193 .deploy(&mut deployment);
2194
2195 deployment.deploy().await.unwrap();
2196
2197 let mut external_in = nodes.connect(input_port).await;
2198 let mut external_out = nodes.connect(out).await;
2199
2200 deployment.start().await.unwrap();
2201
2202 assert_eq!(
2203 external_out.next().await.unwrap(),
2204 std::collections::HashMap::new()
2205 );
2206
2207 external_in.send((1, 1)).await.unwrap();
2208 assert_eq!(
2209 external_out.next().await.unwrap(),
2210 vec![(1, 1)].into_iter().collect()
2211 );
2212
2213 external_in.send((1, 2)).await.unwrap();
2214 assert_eq!(
2215 external_out.next().await.unwrap(),
2216 vec![(1, 2)].into_iter().collect()
2217 );
2218
2219 external_in.send((2, 2)).await.unwrap();
2220 assert_eq!(
2221 external_out.next().await.unwrap(),
2222 vec![(1, 2), (2, 1)].into_iter().collect()
2223 );
2224
2225 external_in.send((1, 1)).await.unwrap();
2226 assert_eq!(
2227 external_out.next().await.unwrap(),
2228 vec![(1, 3), (2, 1)].into_iter().collect()
2229 );
2230
2231 external_in.send((3, 1)).await.unwrap();
2232 assert_eq!(
2233 external_out.next().await.unwrap(),
2234 vec![(1, 3), (2, 1), (3, 1)].into_iter().collect()
2235 );
2236 }
2237
2238 #[cfg(feature = "sim")]
2239 #[test]
2240 fn sim_unbounded_singleton_snapshot() {
2241 let mut flow = FlowBuilder::new();
2242 let node = flow.process::<()>();
2243
2244 let (input_port, input) = node.sim_input();
2245 let output = input
2246 .into_keyed()
2247 .fold(q!(|| 0), q!(|acc, _| *acc += 1))
2248 .snapshot(&node.tick(), nondet!(/** test */))
2249 .entries()
2250 .all_ticks()
2251 .sim_output();
2252
2253 let count = flow.sim().exhaustive(async || {
2254 input_port.send((1, 123));
2255 input_port.send((1, 456));
2256 input_port.send((2, 123));
2257
2258 let all = output.collect_sorted::<Vec<_>>().await;
2259 assert_eq!(all.last().unwrap(), &(2, 1));
2260 });
2261
2262 assert_eq!(count, 8);
2263 }
2264
2265 #[cfg(feature = "deploy")]
2266 #[tokio::test]
2267 async fn join_keyed_stream() {
2268 let mut deployment = Deployment::new();
2269
2270 let mut flow = FlowBuilder::new();
2271 let node = flow.process::<()>();
2272 let external = flow.external::<()>();
2273
2274 let tick = node.tick();
2275 let keyed_data = node
2276 .source_iter(q!(vec![(1, 10), (2, 20)]))
2277 .into_keyed()
2278 .batch(&tick, nondet!(/** test */))
2279 .first();
2280 let requests = node
2281 .source_iter(q!(vec![(1, 100), (2, 200), (3, 300)]))
2282 .into_keyed()
2283 .batch(&tick, nondet!(/** test */));
2284
2285 let out = keyed_data
2286 .join_keyed_stream(requests)
2287 .entries()
2288 .all_ticks()
2289 .send_bincode_external(&external);
2290
2291 let nodes = flow
2292 .with_process(&node, deployment.Localhost())
2293 .with_external(&external, deployment.Localhost())
2294 .deploy(&mut deployment);
2295
2296 deployment.deploy().await.unwrap();
2297
2298 let mut external_out = nodes.connect(out).await;
2299
2300 deployment.start().await.unwrap();
2301
2302 let mut results = vec![];
2303 for _ in 0..2 {
2304 results.push(external_out.next().await.unwrap());
2305 }
2306 results.sort();
2307
2308 assert_eq!(results, vec![(1, (10, 100)), (2, (20, 200))]);
2309 }
2310
2311 #[cfg(feature = "sim")]
2312 #[test]
2313 fn threshold_greater_or_equal_monotonic() {
2314 let mut flow = FlowBuilder::new();
2315 let node = flow.process::<()>();
2316
2317 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2318 let (thresh_port, thresh_input) = node.sim_input::<(u32, usize), _, _>();
2319
2320 // Create a monotonically increasing keyed singleton via fold with monotone proof
2321 let counts: super::KeyedSingleton<u32, usize, _, super::MonotonicValue> =
2322 input.into_keyed().fold(
2323 q!(|| 0usize),
2324 q!(
2325 |acc, v| *acc += v,
2326 monotone = crate::properties::manual_proof!(/** += is monotonic */)
2327 ),
2328 );
2329
2330 // BoundedValue keyed singleton of thresholds (from .first() on unbounded stream)
2331 let thresholds = thresh_input.into_keyed().first();
2332
2333 let output = counts
2334 .threshold_greater_or_equal(thresholds)
2335 .entries()
2336 .sim_output();
2337
2338 let count = flow.sim().exhaustive(async || {
2339 // Set thresholds: key 1 needs value >= 5, key 2 needs value >= 10
2340 thresh_port.send((1, 5));
2341 thresh_port.send((2, 10));
2342
2343 // key 1 gets increments: 3 + 3 = 6, which is >= 5 ✓
2344 input_port.send((1, 3));
2345 input_port.send((1, 3));
2346 // key 2 gets increments: 3 + 3 = 6, which is < 10 ✗
2347 input_port.send((2, 3));
2348 input_port.send((2, 3));
2349
2350 let results = output.collect_sorted::<Vec<_>>().await;
2351 assert_eq!(results, vec![(1, 5)]);
2352 });
2353
2354 assert!(count > 0);
2355 }
2356
2357 #[cfg(feature = "sim")]
2358 #[test]
2359 fn threshold_greater_or_equal_uniform() {
2360 let mut flow = FlowBuilder::new();
2361 let node = flow.process::<()>();
2362
2363 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2364
2365 let counts: super::KeyedSingleton<u32, usize, _, super::MonotonicValue> =
2366 input.into_keyed().fold(
2367 q!(|| 0usize),
2368 q!(
2369 |acc, v| *acc += v,
2370 monotone = crate::properties::manual_proof!(/** += is monotonic */)
2371 ),
2372 );
2373
2374 // Uniform threshold: all keys need value >= 5
2375 let threshold = node.singleton(q!(5usize));
2376
2377 let output = counts
2378 .threshold_greater_or_equal_uniform(threshold)
2379 .entries()
2380 .sim_output();
2381
2382 let count = flow.sim().exhaustive(async || {
2383 // key 1: 3 + 3 = 6 >= 5 ✓
2384 input_port.send((1, 3));
2385 input_port.send((1, 3));
2386 // key 2: 2 + 2 = 4 < 5 ✗
2387 input_port.send((2, 2));
2388 input_port.send((2, 2));
2389
2390 let results = output.collect_sorted::<Vec<_>>().await;
2391 assert_eq!(results, vec![(1, 5)]);
2392 });
2393
2394 assert!(count > 0);
2395 }
2396
2397 #[cfg(feature = "sim")]
2398 #[test]
2399 fn threshold_greater_or_equal_bounded_value() {
2400 let mut flow = FlowBuilder::new();
2401 let node = flow.process::<()>();
2402
2403 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2404 let (thresh_port, thresh_input) = node.sim_input::<(u32, usize), _, _>();
2405
2406 // BoundedValue keyed singleton (values fixed once per key via .first())
2407 let values = input.into_keyed().first();
2408
2409 // BoundedValue keyed singleton of thresholds
2410 let thresholds = thresh_input.into_keyed().first();
2411
2412 let output = values
2413 .threshold_greater_or_equal(thresholds)
2414 .entries()
2415 .sim_output();
2416
2417 let count = flow.sim().exhaustive(async || {
2418 // Set thresholds: key 1 needs >= 3, key 2 needs >= 10
2419 thresh_port.send((1, 3));
2420 thresh_port.send((2, 10));
2421
2422 // key 1 gets value 5 >= 3 ✓, key 2 gets value 4 < 10 ✗
2423 input_port.send((1, 5));
2424 input_port.send((2, 4));
2425
2426 let results = output.collect_sorted::<Vec<_>>().await;
2427 assert_eq!(results, vec![(1, 3)]);
2428 });
2429
2430 assert!(count > 0);
2431 }
2432
2433 #[cfg(feature = "sim")]
2434 #[test]
2435 fn threshold_greater_or_equal_uniform_bounded_value() {
2436 let mut flow = FlowBuilder::new();
2437 let node = flow.process::<()>();
2438
2439 let (input_port, input) = node.sim_input::<(u32, usize), _, _>();
2440
2441 // BoundedValue keyed singleton (values fixed once per key via .first())
2442 let values = input.into_keyed().first();
2443
2444 // Uniform threshold: all keys need value >= 5
2445 let threshold = node.singleton(q!(5usize));
2446
2447 let output = values
2448 .threshold_greater_or_equal_uniform(threshold)
2449 .entries()
2450 .sim_output();
2451
2452 let count = flow.sim().exhaustive(async || {
2453 // key 1 gets value 7 >= 5 ✓, key 2 gets value 3 < 5 ✗
2454 input_port.send((1, 7));
2455 input_port.send((2, 3));
2456
2457 let results = output.collect_sorted::<Vec<_>>().await;
2458 assert_eq!(results, vec![(1, 5)]);
2459 });
2460
2461 assert!(count > 0);
2462 }
2463
2464 #[cfg(feature = "sim")]
2465 #[test]
2466 fn threshold_greater_or_equal_bounded() {
2467 let mut flow = FlowBuilder::new();
2468 let node = flow.process::<()>();
2469
2470 // Bounded keyed singleton (fully known upfront)
2471 let values = node
2472 .source_iter(q!(vec![(1, 6usize), (2, 4usize)]))
2473 .into_keyed()
2474 .first();
2475
2476 // BoundedValue thresholds (from async source)
2477 let (thresh_port, thresh_input) = node.sim_input::<(u32, usize), _, _>();
2478 let thresholds = thresh_input.into_keyed().first();
2479
2480 let output = values
2481 .threshold_greater_or_equal(thresholds)
2482 .entries()
2483 .sim_output();
2484
2485 let count = flow.sim().exhaustive(async || {
2486 thresh_port.send((1, 5));
2487 thresh_port.send((2, 10));
2488
2489 // key 1: 6 >= 5 ✓, key 2: 4 < 10 ✗
2490 let results = output.collect_sorted::<Vec<_>>().await;
2491 assert_eq!(results, vec![(1, 5)]);
2492 });
2493
2494 assert!(count > 0);
2495 }
2496
2497 #[cfg(feature = "sim")]
2498 #[test]
2499 fn threshold_greater_or_equal_uniform_bounded() {
2500 let mut flow = FlowBuilder::new();
2501 let node = flow.process::<()>();
2502
2503 let values = node
2504 .source_iter(q!(vec![(1, 6usize), (2, 4usize)]))
2505 .into_keyed()
2506 .first();
2507 let threshold = node.singleton(q!(5usize));
2508
2509 let output = values
2510 .threshold_greater_or_equal_uniform(threshold)
2511 .entries()
2512 .sim_output();
2513
2514 let count = flow.sim().exhaustive(async || {
2515 // key 1: 6 >= 5 ✓, key 2: 4 < 5 ✗
2516 let results = output.collect_sorted::<Vec<_>>().await;
2517 assert_eq!(results, vec![(1, 5)]);
2518 });
2519
2520 assert!(count > 0);
2521 }
2522}