Skip to main content

hydro_lang/live_collections/
keyed_singleton.rs

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