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