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