Skip to main content

hydro_lang/live_collections/
keyed_singleton.rs

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