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