hydro_lang/live_collections/optional.rs
1//! Definitions for the [`Optional`] live collection.
2
3use std::cell::RefCell;
4use std::marker::PhantomData;
5use std::ops::Deref;
6use std::rc::Rc;
7
8use sealed::sealed;
9use stageleft::{IntoQuotedMut, QuotedWithContext, q};
10use syn::parse_quote;
11
12use super::OperatorContext;
13use super::boundedness::{Bounded, Boundedness, IsBounded, Unbounded};
14use super::singleton::Singleton;
15use super::stream::{AtLeastOnce, ExactlyOnce, NoOrder, Stream, TotalOrder};
16use crate::compile::builder::{CycleId, FlowState};
17use crate::compile::ir::{
18 CollectionKind, HydroIrOpMetadata, HydroNode, HydroRoot, OptionalBoundKind, SharedNode,
19};
20#[cfg(stageleft_runtime)]
21use crate::forward_handle::{CycleCollection, CycleCollectionWithInitial, ReceiverComplete};
22use crate::forward_handle::{ForwardRef, TickCycle};
23use crate::live_collections::singleton::SingletonBound;
24#[cfg(feature = "tokio")]
25use crate::location::TopLevel;
26#[cfg(stageleft_runtime)]
27use crate::location::dynamic::{DynLocation, LocationId};
28use crate::location::tick::{Atomic, DeferTick};
29use crate::location::{Location, Tick, check_matching_location};
30use crate::nondet::{NonDet, nondet};
31use crate::prelude::KeyedSingleton;
32use crate::properties::{StreamMapFuncAlgebra, ValidMutCommutativityFor, ValidMutIdempotenceFor};
33
34/// A marker trait indicating the boundedness of an [`Optional`].
35///
36/// In addition to [`Bounded`] (immutable) and [`Unbounded`] (arbitrarily mutable, including
37/// becoming null again), this also includes additional variants that constrain how the
38/// optional's *presence* (whether it is null) and value evolve over time.
39///
40/// The currently defined variants form a hierarchy of increasing strength; variants other than
41/// [`Bounded`] erase to [`Unbounded`]:
42/// - [`Unbounded`]: the optional may become null and non-null arbitrarily, with an arbitrary
43/// value whenever it is non-null.
44/// - [`InitNone`]: the optional starts null, but once it becomes non-null it stays
45/// non-null forever; the non-null value may still change arbitrarily.
46pub trait OptionalBound {
47 /// The [`Boundedness`] that this [`Optional`] would be erased to.
48 type UnderlyingBound: Boundedness;
49
50 /// Returns the [`OptionalBoundKind`] corresponding to this type.
51 fn bound_kind() -> OptionalBoundKind;
52}
53
54impl OptionalBound for Unbounded {
55 type UnderlyingBound = Unbounded;
56
57 fn bound_kind() -> OptionalBoundKind {
58 OptionalBoundKind::Unbounded
59 }
60}
61
62impl OptionalBound for Bounded {
63 type UnderlyingBound = Bounded;
64
65 fn bound_kind() -> OptionalBoundKind {
66 OptionalBoundKind::Bounded
67 }
68}
69
70/// Marks that the [`Optional`] is null only initially: once it becomes non-null it will remain
71/// non-null forever, although the non-null value may still change arbitrarily over time.
72///
73/// This erases to [`Unbounded`], since the value (when present) is still asynchronously changing.
74pub enum InitNone {}
75
76impl OptionalBound for InitNone {
77 type UnderlyingBound = Unbounded;
78
79 fn bound_kind() -> OptionalBoundKind {
80 OptionalBoundKind::InitNone
81 }
82}
83
84#[sealed]
85#[diagnostic::on_unimplemented(
86 message = "the optional must be null-only-initially (`InitNone`) or bounded (`Bounded`), but has bound `{Self}`",
87 label = "required here",
88 note = "strengthen the guarantee upstream, or consider a different API",
89 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"
90)]
91/// Marker trait that is implemented for [`OptionalBound`] types that are null only initially:
92/// once the optional becomes non-null it remains non-null (or it is [`Bounded`]).
93pub trait IsInitNone: OptionalBound {}
94
95#[sealed]
96#[diagnostic::do_not_recommend]
97impl IsInitNone for InitNone {}
98
99#[sealed]
100#[diagnostic::do_not_recommend]
101impl<B: IsBounded> IsInitNone for B {}
102
103/// A *nullable* Rust value that can asynchronously change over time.
104///
105/// Optionals are the live collection equivalent of [`Option`]. If the optional is [`Bounded`],
106/// the value is frozen and will not change. But if it is [`Unbounded`], the value will
107/// asynchronously change over time, including becoming present of uninhabited.
108///
109/// Optionals are used in many of the same places as [`Singleton`], but when the value may be
110/// nullable. For example, the first element of a [`Stream`] is exposed as an [`Optional`].
111///
112/// Type Parameters:
113/// - `Type`: the type of the value in this optional (when it is not null)
114/// - `Loc`: the [`Location`] where the optional is materialized
115/// - `Bound`: tracks whether the value is [`Bounded`] (fixed) or [`Unbounded`] (changing
116/// asynchronously) or something in between (see [`OptionalBoundKind`]).
117pub struct Optional<Type, Loc, Bound: OptionalBound> {
118 pub(crate) location: Loc,
119 pub(crate) ir_node: Rc<RefCell<HydroNode>>,
120 pub(crate) flow_state: FlowState,
121
122 _phantom: PhantomData<(Type, Loc, Bound)>,
123}
124
125impl<T, L, B: OptionalBound> Drop for Optional<T, L, B> {
126 fn drop(&mut self) {
127 let ir_node = self.ir_node.replace(HydroNode::Placeholder);
128 if !matches!(ir_node, HydroNode::Placeholder) && !ir_node.is_shared_with_others() {
129 self.flow_state.borrow_mut().try_push_root(HydroRoot::Null {
130 input: Box::new(ir_node),
131 op_metadata: HydroIrOpMetadata::new(),
132 });
133 }
134 }
135}
136
137impl<'a, T, L> From<Optional<T, L, Bounded>> for Optional<T, L, Unbounded>
138where
139 T: Clone,
140 L: Location<'a>,
141{
142 fn from(value: Optional<T, L, Bounded>) -> Self {
143 let tick = value.location().tick();
144 value.clone_into_tick(&tick).latest()
145 }
146}
147
148impl<'a, T, L> From<Optional<T, L, InitNone>> for Optional<T, L, Unbounded>
149where
150 L: Location<'a>,
151{
152 fn from(value: Optional<T, L, InitNone>) -> Self {
153 value.ignore_init_none()
154 }
155}
156
157impl<'a, T, L> DeferTick for Optional<T, Tick<L>, Bounded>
158where
159 L: Location<'a>,
160{
161 fn defer_tick(self) -> Self {
162 Optional::defer_tick(self)
163 }
164}
165
166impl<'a, T, L> CycleCollection<'a, TickCycle> for Optional<T, Tick<L>, Bounded>
167where
168 L: Location<'a>,
169{
170 type Location = Tick<L>;
171
172 fn create_source(cycle_id: CycleId, location: Tick<L>) -> Self {
173 Optional::new(
174 location.clone(),
175 HydroNode::CycleSource {
176 cycle_id,
177 metadata: location.new_node_metadata(Self::collection_kind()),
178 },
179 )
180 }
181}
182
183impl<'a, T, L> CycleCollectionWithInitial<'a, TickCycle> for Optional<T, Tick<L>, Bounded>
184where
185 L: Location<'a>,
186{
187 type Location = Tick<L>;
188
189 fn location(&self) -> &Self::Location {
190 self.location()
191 }
192
193 fn create_source_with_initial(cycle_id: CycleId, initial: Self, location: Tick<L>) -> Self {
194 let from_previous_tick: Optional<T, Tick<L>, Bounded> = Optional::new(
195 location.clone(),
196 HydroNode::DeferTick {
197 input: Box::new(HydroNode::CycleSource {
198 cycle_id,
199 metadata: location.new_node_metadata(Self::collection_kind()),
200 }),
201 metadata: location
202 .new_node_metadata(Optional::<T, Tick<L>, Bounded>::collection_kind()),
203 },
204 );
205
206 from_previous_tick.or(initial.filter_if(location.optional_first_tick(q!(())).is_some()))
207 }
208}
209
210impl<'a, T, L> ReceiverComplete<'a, TickCycle> for Optional<T, Tick<L>, Bounded>
211where
212 L: Location<'a>,
213{
214 fn complete(self, cycle_id: CycleId, expected_location: LocationId) {
215 assert_eq!(
216 Location::id(&self.location),
217 expected_location,
218 "locations do not match"
219 );
220 self.location
221 .flow_state()
222 .borrow_mut()
223 .push_root(HydroRoot::CycleSink {
224 cycle_id,
225 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
226 op_metadata: HydroIrOpMetadata::new(),
227 });
228 }
229}
230
231impl<'a, T, L, B: OptionalBound> CycleCollection<'a, ForwardRef> for Optional<T, L, B>
232where
233 L: Location<'a>,
234{
235 type Location = L;
236
237 fn create_source(cycle_id: CycleId, location: L) -> Self {
238 Optional::new(
239 location.clone(),
240 HydroNode::CycleSource {
241 cycle_id,
242 metadata: location.new_node_metadata(Self::collection_kind()),
243 },
244 )
245 }
246}
247
248impl<'a, T, L, B: OptionalBound> ReceiverComplete<'a, ForwardRef> for Optional<T, L, B>
249where
250 L: Location<'a>,
251{
252 fn complete(self, cycle_id: CycleId, expected_location: LocationId) {
253 assert_eq!(
254 Location::id(&self.location),
255 expected_location,
256 "locations do not match"
257 );
258 self.location
259 .flow_state()
260 .borrow_mut()
261 .push_root(HydroRoot::CycleSink {
262 cycle_id,
263 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
264 op_metadata: HydroIrOpMetadata::new(),
265 });
266 }
267}
268
269impl<'a, T, L, B: SingletonBound> From<Singleton<T, L, B>> for Optional<T, L, B::UnderlyingBound>
270where
271 L: Location<'a>,
272{
273 fn from(singleton: Singleton<T, L, B>) -> Self {
274 Optional::new(
275 singleton.location.clone(),
276 HydroNode::Cast {
277 inner: Box::new(singleton.ir_node.replace(HydroNode::Placeholder)),
278 metadata: singleton
279 .location
280 .new_node_metadata(Self::collection_kind()),
281 },
282 )
283 }
284}
285
286#[cfg(stageleft_runtime)]
287pub(super) fn zip_inside_tick<'a, T, O, L: Location<'a>, B: Boundedness>(
288 me: Optional<T, L, B>,
289 other: Optional<O, L, B>,
290) -> Optional<(T, O), L, B> {
291 check_matching_location(&me.location, &other.location);
292
293 Optional::new(
294 me.location.clone(),
295 HydroNode::CrossSingleton {
296 left: Box::new(me.ir_node.replace(HydroNode::Placeholder)),
297 right: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
298 metadata: me
299 .location
300 .new_node_metadata(Optional::<(T, O), L, B>::collection_kind()),
301 },
302 )
303}
304
305#[cfg(stageleft_runtime)]
306fn or_inside_tick<'a, T, L: Location<'a>, B: Boundedness>(
307 me: Optional<T, L, B>,
308 other: Optional<T, L, B>,
309) -> Optional<T, L, B> {
310 check_matching_location(&me.location, &other.location);
311
312 Optional::new(
313 me.location.clone(),
314 HydroNode::ChainFirst {
315 first: Box::new(me.ir_node.replace(HydroNode::Placeholder)),
316 second: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
317 metadata: me
318 .location
319 .new_node_metadata(Optional::<T, L, B>::collection_kind()),
320 },
321 )
322}
323
324impl<'a, T, L, B: OptionalBound> Clone for Optional<T, L, B>
325where
326 T: Clone,
327 L: Location<'a>,
328{
329 fn clone(&self) -> Self {
330 if !matches!(self.ir_node.borrow().deref(), HydroNode::Tee { .. }) {
331 let orig_ir_node = self.ir_node.replace(HydroNode::Placeholder);
332 *self.ir_node.borrow_mut() = HydroNode::Tee {
333 inner: SharedNode(Rc::new(RefCell::new(orig_ir_node))),
334 metadata: self.location.new_node_metadata(Self::collection_kind()),
335 };
336 }
337
338 if let HydroNode::Tee { inner, metadata } = self.ir_node.borrow().deref() {
339 Optional {
340 location: self.location.clone(),
341 flow_state: self.flow_state.clone(),
342 ir_node: super::tracked_ir_node(
343 &self.flow_state,
344 HydroNode::Tee {
345 inner: SharedNode(inner.0.clone()),
346 metadata: metadata.clone(),
347 },
348 ),
349 _phantom: PhantomData,
350 }
351 } else {
352 unreachable!()
353 }
354 }
355}
356
357impl<'a, T, L, B: OptionalBound> Optional<T, L, B>
358where
359 L: Location<'a>,
360{
361 pub(crate) fn new(location: L, ir_node: HydroNode) -> Self {
362 debug_assert_eq!(ir_node.metadata().location_id, Location::id(&location));
363 debug_assert_eq!(ir_node.metadata().collection_kind, Self::collection_kind());
364 let flow_state = location.flow_state().clone();
365 let ir_node = super::tracked_ir_node(&flow_state, ir_node);
366 Optional {
367 location,
368 flow_state,
369 ir_node,
370 _phantom: PhantomData,
371 }
372 }
373
374 pub(crate) fn collection_kind() -> CollectionKind {
375 CollectionKind::Optional {
376 bound: <B as OptionalBound>::bound_kind(),
377 element_type: stageleft::quote_type::<T>().into(),
378 }
379 }
380
381 /// Returns the [`Location`] where this optional is being materialized.
382 pub fn location(&self) -> &L {
383 &self.location
384 }
385
386 /// Creates a shared reference handle to this optional that can be captured inside `q!()`
387 /// closures. The handle resolves to `&Option<T>` at runtime.
388 ///
389 /// The optional must be bounded, otherwise reading it would be non-deterministic.
390 /// The handle can only be captured in closures passed to operators on collections at
391 /// the same location with **matching boundedness**; capturing it in a closure over an
392 /// unbounded collection is rejected at compile time.
393 pub fn by_ref(&self) -> crate::handoff_ref::OptionalRef<'a, '_, T, L, B>
394 where
395 B: IsBounded,
396 {
397 crate::handoff_ref::OptionalRef::new(&self.ir_node)
398 }
399
400 /// Returns a mutable reference handle to this optional that can be captured inside `q!()`
401 /// closures. The handle resolves to `&mut Option<T>` at runtime.
402 pub fn by_mut(&self) -> crate::handoff_ref::OptionalMut<'a, '_, T, L, B>
403 where
404 B: IsBounded,
405 {
406 crate::handoff_ref::OptionalMut::new(&self.ir_node)
407 }
408
409 /// Weakens the consistency of this live collection to not guarantee any consistency across
410 /// cluster members (if this collection is on a cluster).
411 pub fn weaken_consistency(self) -> Optional<T, L::DropConsistency, B>
412 where
413 L: Location<'a>,
414 {
415 if L::consistency()
416 .is_none_or(|c| c == crate::location::dynamic::ClusterConsistency::NoConsistency)
417 {
418 // already no consistency
419 Optional::new(
420 self.location.drop_consistency(),
421 self.ir_node.replace(HydroNode::Placeholder),
422 )
423 } else {
424 Optional::new(
425 self.location.drop_consistency(),
426 HydroNode::Cast {
427 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
428 metadata: self
429 .location
430 .clone()
431 .drop_consistency()
432 .new_node_metadata(Optional::<T, L::DropConsistency, B>::collection_kind()),
433 },
434 )
435 }
436 }
437
438 /// Casts this live collection to have the consistency guarantees specified in the given
439 /// location type parameter. The developer must ensure that the strengthened consistency
440 /// is actually guaranteed, via the proof field (see [`crate::prelude::manual_proof`]).
441 pub fn assert_has_consistency_of<L2: Location<'a, DropConsistency = L::DropConsistency>>(
442 self,
443 _proof: impl crate::properties::ConsistencyProof,
444 ) -> Optional<T, L2, B>
445 where
446 L: Location<'a>,
447 {
448 if L::consistency() == L2::consistency() {
449 Optional::new(
450 self.location.with_consistency_of(),
451 self.ir_node.replace(HydroNode::Placeholder),
452 )
453 } else {
454 Optional::new(
455 self.location.with_consistency_of(),
456 HydroNode::AssertIsConsistent {
457 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
458 trusted: false,
459 metadata: self
460 .location
461 .clone()
462 .with_consistency_of::<L2>()
463 .new_node_metadata(Optional::<T, L2, B>::collection_kind()),
464 },
465 )
466 }
467 }
468
469 /// Transforms the optional value by applying a function `f` to it,
470 /// continuously as the input is updated.
471 ///
472 /// Whenever the optional is empty, the output optional is also empty.
473 ///
474 /// # Example
475 /// ```rust
476 /// # #[cfg(feature = "deploy")] {
477 /// # use hydro_lang::prelude::*;
478 /// # use futures::StreamExt;
479 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
480 /// let tick = process.tick();
481 /// let optional = tick.optional_first_tick(q!(1));
482 /// optional.map(q!(|v| v + 1)).all_ticks()
483 /// # }, |mut stream| async move {
484 /// // 2
485 /// # assert_eq!(stream.next().await.unwrap(), 2);
486 /// # }));
487 /// # }
488 /// ```
489 pub fn map<U, F>(
490 self,
491 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>>,
492 ) -> Optional<U, L, B>
493 where
494 F: Fn(T) -> U + 'a,
495 {
496 let f = f
497 .splice_fn1_ctx(&OperatorContext::<L, B::UnderlyingBound>::new(
498 &self.location,
499 ))
500 .into();
501 Optional::new(
502 self.location.clone(),
503 HydroNode::Map {
504 f,
505 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
506 metadata: self
507 .location
508 .new_node_metadata(Optional::<U, L, B>::collection_kind()),
509 },
510 )
511 }
512
513 /// Transforms the optional value by applying a function `f` to it and then flattening
514 /// the result into a stream, preserving the order of elements.
515 ///
516 /// If the optional is empty, the output stream is also empty. If the optional contains
517 /// a value, `f` is applied to produce an iterator, and all items from that iterator
518 /// are emitted in the output stream in deterministic order.
519 ///
520 /// The implementation of [`Iterator`] for the output type `I` must produce items in a
521 /// **deterministic** order. For example, `I` could be a `Vec`, but not a `HashSet`.
522 /// If the order is not deterministic, use [`Optional::flat_map_unordered`] instead.
523 ///
524 /// # Example
525 /// ```rust
526 /// # #[cfg(feature = "deploy")] {
527 /// # use hydro_lang::prelude::*;
528 /// # use futures::StreamExt;
529 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
530 /// let tick = process.tick();
531 /// let optional = tick.optional_first_tick(q!(vec![1, 2, 3]));
532 /// optional.flat_map_ordered(q!(|v| v)).all_ticks()
533 /// # }, |mut stream| async move {
534 /// // 1, 2, 3
535 /// # for w in vec![1, 2, 3] {
536 /// # assert_eq!(stream.next().await.unwrap(), w);
537 /// # }
538 /// # }));
539 /// # }
540 /// ```
541 pub fn flat_map_ordered<U, I, F, C, Idemp, const WAS_MUT: bool>(
542 self,
543 f: impl IntoQuotedMut<
544 'a,
545 F,
546 OperatorContext<L, Bounded>,
547 StreamMapFuncAlgebra<T, Bounded, C, Idemp, L::SimHookScope>,
548 >,
549 ) -> Stream<U, L, Bounded, TotalOrder, ExactlyOnce>
550 where
551 B: IsBounded,
552 I: IntoIterator<Item = U>,
553 F: FnMut(T) -> I + 'a,
554 C: ValidMutCommutativityFor<F, T, I, TotalOrder, WAS_MUT>,
555 Idemp: ValidMutIdempotenceFor<F, T, I, ExactlyOnce, WAS_MUT>,
556 {
557 self.into_stream().flat_map_ordered(f)
558 }
559
560 /// Like [`Optional::flat_map_ordered`], but allows the implementation of [`Iterator`]
561 /// for the output type `I` to produce items in any order.
562 ///
563 /// If the optional is empty, the output stream is also empty. If the optional contains
564 /// a value, `f` is applied to produce an iterator, and all items from that iterator
565 /// are emitted in the output stream in non-deterministic order.
566 ///
567 /// # Example
568 /// ```rust
569 /// # #[cfg(feature = "deploy")] {
570 /// # use hydro_lang::{prelude::*, live_collections::stream::{NoOrder, ExactlyOnce}};
571 /// # use futures::StreamExt;
572 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test::<_, _, _, NoOrder, ExactlyOnce>(|process| {
573 /// let tick = process.tick();
574 /// let optional = tick.optional_first_tick(q!(
575 /// std::collections::HashSet::<i32>::from_iter(vec![1, 2, 3])
576 /// ));
577 /// optional.flat_map_unordered(q!(|v| v)).all_ticks()
578 /// # }, |mut stream| async move {
579 /// // 1, 2, 3, but in no particular order
580 /// # let mut results = Vec::new();
581 /// # for _ in 0..3 {
582 /// # results.push(stream.next().await.unwrap());
583 /// # }
584 /// # results.sort();
585 /// # assert_eq!(results, vec![1, 2, 3]);
586 /// # }));
587 /// # }
588 /// ```
589 pub fn flat_map_unordered<U, I, F, C, Idemp, const WAS_MUT: bool>(
590 self,
591 f: impl IntoQuotedMut<
592 'a,
593 F,
594 OperatorContext<L, Bounded>,
595 StreamMapFuncAlgebra<T, Bounded, C, Idemp, L::SimHookScope>,
596 >,
597 ) -> Stream<U, L, Bounded, NoOrder, ExactlyOnce>
598 where
599 B: IsBounded,
600 I: IntoIterator<Item = U>,
601 F: FnMut(T) -> I + 'a,
602 C: ValidMutCommutativityFor<F, T, I, TotalOrder, WAS_MUT>,
603 Idemp: ValidMutIdempotenceFor<F, T, I, ExactlyOnce, WAS_MUT>,
604 {
605 self.into_stream().flat_map_unordered(f)
606 }
607
608 /// Flattens the optional value into a stream, preserving the order of elements.
609 ///
610 /// If the optional is empty, the output stream is also empty. If the optional contains
611 /// a value that implements [`IntoIterator`], all items from that iterator are emitted
612 /// in the output stream in deterministic order.
613 ///
614 /// The implementation of [`Iterator`] for the element type `T` must produce items in a
615 /// **deterministic** order. For example, `T` could be a `Vec`, but not a `HashSet`.
616 /// If the order is not deterministic, use [`Optional::flatten_unordered`] instead.
617 ///
618 /// # Example
619 /// ```rust
620 /// # #[cfg(feature = "deploy")] {
621 /// # use hydro_lang::prelude::*;
622 /// # use futures::StreamExt;
623 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
624 /// let tick = process.tick();
625 /// let optional = tick.optional_first_tick(q!(vec![1, 2, 3]));
626 /// optional.flatten_ordered().all_ticks()
627 /// # }, |mut stream| async move {
628 /// // 1, 2, 3
629 /// # for w in vec![1, 2, 3] {
630 /// # assert_eq!(stream.next().await.unwrap(), w);
631 /// # }
632 /// # }));
633 /// # }
634 /// ```
635 pub fn flatten_ordered<U>(self) -> Stream<U, L, Bounded, TotalOrder, ExactlyOnce>
636 where
637 B: IsBounded,
638 T: IntoIterator<Item = U>,
639 {
640 self.flat_map_ordered(q!(|v| v))
641 }
642
643 /// Like [`Optional::flatten_ordered`], but allows the implementation of [`Iterator`]
644 /// for the element type `T` to produce items in any order.
645 ///
646 /// If the optional is empty, the output stream is also empty. If the optional contains
647 /// a value that implements [`IntoIterator`], all items from that iterator are emitted
648 /// in the output stream in non-deterministic order.
649 ///
650 /// # Example
651 /// ```rust
652 /// # #[cfg(feature = "deploy")] {
653 /// # use hydro_lang::{prelude::*, live_collections::stream::{NoOrder, ExactlyOnce}};
654 /// # use futures::StreamExt;
655 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test::<_, _, _, NoOrder, ExactlyOnce>(|process| {
656 /// let tick = process.tick();
657 /// let optional = tick.optional_first_tick(q!(
658 /// std::collections::HashSet::<i32>::from_iter(vec![1, 2, 3])
659 /// ));
660 /// optional.flatten_unordered().all_ticks()
661 /// # }, |mut stream| async move {
662 /// // 1, 2, 3, but in no particular order
663 /// # let mut results = Vec::new();
664 /// # for _ in 0..3 {
665 /// # results.push(stream.next().await.unwrap());
666 /// # }
667 /// # results.sort();
668 /// # assert_eq!(results, vec![1, 2, 3]);
669 /// # }));
670 /// # }
671 /// ```
672 pub fn flatten_unordered<U>(self) -> Stream<U, L, Bounded, NoOrder, ExactlyOnce>
673 where
674 B: IsBounded,
675 T: IntoIterator<Item = U>,
676 {
677 self.flat_map_unordered(q!(|v| v))
678 }
679
680 /// Creates an optional containing only the value if it satisfies a predicate `f`.
681 ///
682 /// If the optional is empty, the output optional is also empty. If the optional contains
683 /// a value and the predicate returns `true`, the output optional contains the same value.
684 /// If the predicate returns `false`, the output optional is empty.
685 ///
686 /// The closure `f` receives a reference `&T` rather than an owned value `T` because filtering does
687 /// not modify or take ownership of the value. If you need to modify the value while filtering
688 /// use [`Optional::filter_map`] instead.
689 ///
690 /// # Example
691 /// ```rust
692 /// # #[cfg(feature = "deploy")] {
693 /// # use hydro_lang::prelude::*;
694 /// # use futures::StreamExt;
695 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
696 /// let tick = process.tick();
697 /// let optional = tick.optional_first_tick(q!(5));
698 /// optional.filter(q!(|&x| x > 3)).all_ticks()
699 /// # }, |mut stream| async move {
700 /// // 5
701 /// # assert_eq!(stream.next().await.unwrap(), 5);
702 /// # }));
703 /// # }
704 /// ```
705 pub fn filter<F>(
706 self,
707 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>>,
708 ) -> Optional<T, L, B::UnderlyingBound>
709 where
710 F: Fn(&T) -> bool + 'a,
711 {
712 let f = f
713 .splice_fn1_borrow_ctx(&OperatorContext::<L, B::UnderlyingBound>::new(
714 &self.location,
715 ))
716 .into();
717 Optional::new(
718 self.location.clone(),
719 HydroNode::Filter {
720 f,
721 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
722 metadata: self
723 .location
724 .new_node_metadata(Optional::<T, L, B::UnderlyingBound>::collection_kind()),
725 },
726 )
727 }
728
729 /// An operator that both filters and maps. It yields only the value if the supplied
730 /// closure `f` returns `Some(value)`.
731 ///
732 /// If the optional is empty, the output optional is also empty. If the optional contains
733 /// a value and the closure returns `Some(new_value)`, the output optional contains `new_value`.
734 /// If the closure returns `None`, the output optional is empty.
735 ///
736 /// # Example
737 /// ```rust
738 /// # #[cfg(feature = "deploy")] {
739 /// # use hydro_lang::prelude::*;
740 /// # use futures::StreamExt;
741 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
742 /// let tick = process.tick();
743 /// let optional = tick.optional_first_tick(q!("42"));
744 /// optional
745 /// .filter_map(q!(|s| s.parse::<i32>().ok()))
746 /// .all_ticks()
747 /// # }, |mut stream| async move {
748 /// // 42
749 /// # assert_eq!(stream.next().await.unwrap(), 42);
750 /// # }));
751 /// # }
752 /// ```
753 pub fn filter_map<U, F>(
754 self,
755 f: impl IntoQuotedMut<'a, F, OperatorContext<L, B::UnderlyingBound>>,
756 ) -> Optional<U, L, B::UnderlyingBound>
757 where
758 F: Fn(T) -> Option<U> + 'a,
759 {
760 let f = f
761 .splice_fn1_ctx(&OperatorContext::<L, B::UnderlyingBound>::new(
762 &self.location,
763 ))
764 .into();
765 Optional::new(
766 self.location.clone(),
767 HydroNode::FilterMap {
768 f,
769 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
770 metadata: self
771 .location
772 .new_node_metadata(Optional::<U, L, B::UnderlyingBound>::collection_kind()),
773 },
774 )
775 }
776
777 /// Combines this singleton with another [`Singleton`] or [`Optional`] by tupling their values.
778 ///
779 /// If the other value is a [`Optional`], the output will be non-null only if the argument is
780 /// non-null. This is useful for combining several pieces of state together.
781 ///
782 /// # Example
783 /// ```rust
784 /// # #[cfg(feature = "deploy")] {
785 /// # use hydro_lang::prelude::*;
786 /// # use futures::StreamExt;
787 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
788 /// let tick = process.tick();
789 /// let numbers = process
790 /// .source_iter(q!(vec![123, 456, 789]))
791 /// .batch(&tick, nondet!(/** test */));
792 /// let min = numbers.clone().min(); // Optional
793 /// let max = numbers.max(); // Optional
794 /// min.zip(max).all_ticks()
795 /// # }, |mut stream| async move {
796 /// // [(123, 789)]
797 /// # for w in vec![(123, 789)] {
798 /// # assert_eq!(stream.next().await.unwrap(), w);
799 /// # }
800 /// # }));
801 /// # }
802 /// ```
803 pub fn zip<O>(self, other: impl Into<Optional<O, L, B>>) -> Optional<(T, O), L, B>
804 where
805 B: IsBounded,
806 {
807 let other: Optional<O, L, B> = other.into();
808 check_matching_location(&self.location, &other.location);
809
810 if L::is_top_level()
811 && let Some(tick) = self.location.try_tick()
812 {
813 let self_location = self.location().clone();
814 let out = zip_inside_tick(
815 self.snapshot(&tick, nondet!(/** eventually stabilizes */)),
816 other.snapshot(&tick, nondet!(/** eventually stabilizes */)),
817 )
818 .latest();
819
820 // `latest()` produces an `Unbounded` optional; cast it back to this method's
821 // (bounded) output kind. The cast is not just metadata: at the root, unbounded
822 // optionals are tombstoned update feeds while bounded ones are plain values.
823 Optional::new(
824 self_location.clone(),
825 HydroNode::Cast {
826 inner: Box::new(out.ir_node.replace(HydroNode::Placeholder)),
827 metadata: self_location
828 .new_node_metadata(Optional::<(T, O), L, B>::collection_kind()),
829 },
830 )
831 } else {
832 zip_inside_tick(self, other)
833 }
834 }
835
836 /// Passes through `self` when it has a value, otherwise passes through `other`.
837 ///
838 /// Like [`Option::or`], this is helpful for defining a fallback for an [`Optional`], when the
839 /// fallback itself is an [`Optional`]. If the fallback is a [`Singleton`], you can use
840 /// [`Optional::unwrap_or`] to ensure that the output is always non-null.
841 ///
842 /// If the inputs are [`Unbounded`], the output will be asynchronously updated as the contents
843 /// of the inputs change (including to/from null states).
844 ///
845 /// # Example
846 /// ```rust
847 /// # #[cfg(feature = "deploy")] {
848 /// # use hydro_lang::prelude::*;
849 /// # use futures::StreamExt;
850 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
851 /// let tick = process.tick();
852 /// // ticks are lazy by default, forces the second tick to run
853 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
854 ///
855 /// let some_first_tick = tick.optional_first_tick(q!(123));
856 /// let some_second_tick = tick.optional_first_tick(q!(456)).defer_tick();
857 /// some_first_tick.or(some_second_tick).all_ticks()
858 /// # }, |mut stream| async move {
859 /// // [123 /* first tick */, 456 /* second tick */]
860 /// # for w in vec![123, 456] {
861 /// # assert_eq!(stream.next().await.unwrap(), w);
862 /// # }
863 /// # }));
864 /// # }
865 /// ```
866 pub fn or(
867 self,
868 other: Optional<T, L, B::UnderlyingBound>,
869 ) -> Optional<T, L, B::UnderlyingBound> {
870 let me = self.ignore_init_none();
871 check_matching_location(&me.location, &other.location);
872
873 if L::is_top_level()
874 && !<B::UnderlyingBound as Boundedness>::BOUNDED // only if unbounded we need to use a tick
875 && let Some(tick) = me.location.try_tick()
876 {
877 let self_location = me.location().clone();
878 let out = or_inside_tick(
879 me.snapshot(&tick, nondet!(/** eventually stabilizes */)),
880 other.snapshot(&tick, nondet!(/** eventually stabilizes */)),
881 )
882 .latest();
883
884 Optional::new(self_location, out.ir_node.replace(HydroNode::Placeholder))
885 } else {
886 Optional::new(
887 me.location.clone(),
888 HydroNode::ChainFirst {
889 first: Box::new(me.ir_node.replace(HydroNode::Placeholder)),
890 second: Box::new(other.ir_node.replace(HydroNode::Placeholder)),
891 metadata: me
892 .location
893 .new_node_metadata(Optional::<T, L, B::UnderlyingBound>::collection_kind()),
894 },
895 )
896 }
897 }
898
899 /// Gets the contents of `self` when it has a value, otherwise passes through `other`.
900 ///
901 /// Like [`Option::unwrap_or`], this is helpful for defining a fallback for an [`Optional`].
902 /// If the fallback is not always defined (an [`Optional`]), you can use [`Optional::or`].
903 ///
904 /// If the inputs are [`Unbounded`], the output will be asynchronously updated as the contents
905 /// of the inputs change (including to/from null states).
906 ///
907 /// # Example
908 /// ```rust
909 /// # #[cfg(feature = "deploy")] {
910 /// # use hydro_lang::prelude::*;
911 /// # use futures::StreamExt;
912 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
913 /// let tick = process.tick();
914 /// // ticks are lazy by default, forces the later ticks to run
915 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
916 ///
917 /// let some_first_tick = tick.optional_first_tick(q!(123));
918 /// some_first_tick
919 /// .unwrap_or(tick.singleton(q!(456)))
920 /// .all_ticks()
921 /// # }, |mut stream| async move {
922 /// // [123 /* first tick */, 456 /* second tick */, 456 /* third tick */, 456, ...]
923 /// # for w in vec![123, 456, 456, 456] {
924 /// # assert_eq!(stream.next().await.unwrap(), w);
925 /// # }
926 /// # }));
927 /// # }
928 /// ```
929 pub fn unwrap_or(
930 self,
931 other: Singleton<T, L, B::UnderlyingBound>,
932 ) -> Singleton<T, L, B::UnderlyingBound> {
933 let res_option = self.or(other.into());
934 Singleton::new(
935 res_option.location.clone(),
936 HydroNode::Cast {
937 inner: Box::new(res_option.ir_node.replace(HydroNode::Placeholder)),
938 metadata: res_option.location.new_node_metadata(Singleton::<
939 T,
940 L,
941 B::UnderlyingBound,
942 >::collection_kind(
943 )),
944 },
945 )
946 }
947
948 /// Gets the contents of `self` when it has a value, otherwise returns the default value of `T`.
949 ///
950 /// Like [`Option::unwrap_or_default`], this is helpful for defining a fallback for an
951 /// [`Optional`] when the default value of the type is a suitable fallback.
952 ///
953 /// # Example
954 /// ```rust
955 /// # #[cfg(feature = "deploy")] {
956 /// # use hydro_lang::prelude::*;
957 /// # use futures::StreamExt;
958 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
959 /// let tick = process.tick();
960 /// // ticks are lazy by default, forces the later ticks to run
961 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
962 ///
963 /// let some_first_tick = tick.optional_first_tick(q!(123i32));
964 /// some_first_tick.unwrap_or_default().all_ticks()
965 /// # }, |mut stream| async move {
966 /// // [123 /* first tick */, 0 /* second tick */, 0 /* third tick */, 0, ...]
967 /// # for w in vec![123, 0, 0, 0] {
968 /// # assert_eq!(stream.next().await.unwrap(), w);
969 /// # }
970 /// # }));
971 /// # }
972 /// ```
973 pub fn unwrap_or_default(self) -> Singleton<T, L, B::UnderlyingBound>
974 where
975 T: Default + Clone,
976 {
977 self.into_singleton().map(q!(|v| v.unwrap_or_default()))
978 }
979
980 /// Converts this optional into a [`Singleton`] with a Rust [`Option`] as its contents.
981 ///
982 /// Useful for writing custom Rust code that needs to interact with both the null and non-null
983 /// states of the [`Optional`]. When possible, you should use the native APIs on [`Optional`]
984 /// so that Hydro can skip any computation on null values.
985 ///
986 /// # Example
987 /// ```rust
988 /// # #[cfg(feature = "deploy")] {
989 /// # use hydro_lang::prelude::*;
990 /// # use futures::StreamExt;
991 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
992 /// let tick = process.tick();
993 /// // ticks are lazy by default, forces the later ticks to run
994 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
995 ///
996 /// let some_first_tick = tick.optional_first_tick(q!(123));
997 /// some_first_tick.into_singleton().all_ticks()
998 /// # }, |mut stream| async move {
999 /// // [Some(123) /* first tick */, None /* second tick */, None /* third tick */, None, ...]
1000 /// # for w in vec![Some(123), None, None, None] {
1001 /// # assert_eq!(stream.next().await.unwrap(), w);
1002 /// # }
1003 /// # }));
1004 /// # }
1005 /// ```
1006 pub fn into_singleton(self) -> Singleton<Option<T>, L, B::UnderlyingBound>
1007 where
1008 T: Clone,
1009 {
1010 let none: syn::Expr = parse_quote!(::std::option::Option::None);
1011
1012 let none_singleton = Singleton::new(
1013 self.location.clone(),
1014 HydroNode::SingletonSource {
1015 value: none.into(),
1016 first_tick_only: false,
1017 metadata: self.location.new_node_metadata(Singleton::<
1018 Option<T>,
1019 L,
1020 B::UnderlyingBound,
1021 >::collection_kind()),
1022 },
1023 );
1024
1025 self.map(q!(|v| Some(v))).unwrap_or(none_singleton)
1026 }
1027
1028 /// Returns a [`Singleton`] containing `true` if this optional has a value, `false` otherwise.
1029 ///
1030 /// # Example
1031 /// ```rust
1032 /// # #[cfg(feature = "deploy")] {
1033 /// # use hydro_lang::prelude::*;
1034 /// # use futures::StreamExt;
1035 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1036 /// let tick = process.tick();
1037 /// // ticks are lazy by default, forces the second tick to run
1038 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1039 ///
1040 /// let some_first_tick = tick.optional_first_tick(q!(42));
1041 /// some_first_tick.is_some().all_ticks()
1042 /// # }, |mut stream| async move {
1043 /// // [true /* first tick */, false /* second tick */, ...]
1044 /// # for w in vec![true, false] {
1045 /// # assert_eq!(stream.next().await.unwrap(), w);
1046 /// # }
1047 /// # }));
1048 /// # }
1049 /// ```
1050 #[expect(clippy::wrong_self_convention, reason = "Stream naming")]
1051 pub fn is_some(self) -> Singleton<bool, L, B::UnderlyingBound> {
1052 self.map(q!(|_| ()))
1053 .into_singleton()
1054 .map(q!(|o| o.is_some()))
1055 }
1056
1057 /// Returns a [`Singleton`] containing `true` if this optional is null, `false` otherwise.
1058 ///
1059 /// # Example
1060 /// ```rust
1061 /// # #[cfg(feature = "deploy")] {
1062 /// # use hydro_lang::prelude::*;
1063 /// # use futures::StreamExt;
1064 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1065 /// let tick = process.tick();
1066 /// // ticks are lazy by default, forces the second tick to run
1067 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1068 ///
1069 /// let some_first_tick = tick.optional_first_tick(q!(42));
1070 /// some_first_tick.is_none().all_ticks()
1071 /// # }, |mut stream| async move {
1072 /// // [false /* first tick */, true /* second tick */, ...]
1073 /// # for w in vec![false, true] {
1074 /// # assert_eq!(stream.next().await.unwrap(), w);
1075 /// # }
1076 /// # }));
1077 /// # }
1078 /// ```
1079 #[expect(clippy::wrong_self_convention, reason = "Stream naming")]
1080 pub fn is_none(self) -> Singleton<bool, L, B::UnderlyingBound> {
1081 self.map(q!(|_| ()))
1082 .into_singleton()
1083 .map(q!(|o| o.is_none()))
1084 }
1085
1086 /// Returns a [`Singleton`] containing `true` if both optionals are non-null and their
1087 /// values are equal, `false` otherwise (including when either is null).
1088 ///
1089 /// # Example
1090 /// ```rust
1091 /// # #[cfg(feature = "deploy")] {
1092 /// # use hydro_lang::prelude::*;
1093 /// # use futures::StreamExt;
1094 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1095 /// let tick = process.tick();
1096 /// // ticks are lazy by default, forces the second tick to run
1097 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1098 ///
1099 /// let a = tick.optional_first_tick(q!(5)); // Some(5), None
1100 /// let b = tick.optional_first_tick(q!(5)); // Some(5), None
1101 /// a.is_some_and_equals(b).all_ticks()
1102 /// # }, |mut stream| async move {
1103 /// // [true, false]
1104 /// # for w in vec![true, false] {
1105 /// # assert_eq!(stream.next().await.unwrap(), w);
1106 /// # }
1107 /// # }));
1108 /// # }
1109 /// ```
1110 #[expect(clippy::wrong_self_convention, reason = "Stream naming")]
1111 pub fn is_some_and_equals(self, other: Optional<T, L, B>) -> Singleton<bool, L, B>
1112 where
1113 T: PartialEq + Clone,
1114 B: IsBounded,
1115 {
1116 self.into_singleton()
1117 .zip(other.into_singleton())
1118 .map(q!(|(a, b)| a.is_some() && a == b))
1119 }
1120
1121 /// An operator which allows you to "name" a `HydroNode`.
1122 /// This is only used for testing, to correlate certain `HydroNode`s with IDs.
1123 pub fn ir_node_named(self, name: &str) -> Optional<T, L, B> {
1124 {
1125 let mut node = self.ir_node.borrow_mut();
1126 let metadata = node.metadata_mut();
1127 metadata.tag = Some(name.to_owned());
1128 }
1129 self
1130 }
1131
1132 /// Drops the [`InitNone`] guarantee of the [`Optional`], erasing it to its
1133 /// [`OptionalBound::UnderlyingBound`] (i.e. [`Unbounded`] for [`InitNone`]).
1134 pub fn ignore_init_none(self) -> Optional<T, L, B::UnderlyingBound> {
1135 if <B as OptionalBound>::bound_kind() == <B::UnderlyingBound as OptionalBound>::bound_kind()
1136 {
1137 Optional::new(
1138 self.location.clone(),
1139 self.ir_node.replace(HydroNode::Placeholder),
1140 )
1141 } else {
1142 Optional::new(
1143 self.location.clone(),
1144 HydroNode::Cast {
1145 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1146 metadata: self
1147 .location
1148 .new_node_metadata(Optional::<T, L, B::UnderlyingBound>::collection_kind()),
1149 },
1150 )
1151 }
1152 }
1153
1154 /// Strengthens the boundedness guarantee to `Bounded`, given that `B: IsBounded`, which
1155 /// implies that `B == Bounded`.
1156 pub fn make_bounded(self) -> Optional<T, L, Bounded>
1157 where
1158 B: IsBounded,
1159 {
1160 Optional::new(
1161 self.location.clone(),
1162 self.ir_node.replace(HydroNode::Placeholder),
1163 )
1164 }
1165
1166 /// Clones this bounded optional into a tick, returning a optional that has the
1167 /// same value as the outer optional. Because the outer optional is bounded, this
1168 /// is deterministic because there is only a single immutable version.
1169 pub fn clone_into_tick(self, tick: &Tick<L>) -> Optional<T, Tick<L>, Bounded>
1170 where
1171 B: IsBounded,
1172 T: Clone,
1173 {
1174 // TODO(shadaj): avoid printing simulator logs for this snapshot
1175 let inner = self.snapshot(
1176 tick,
1177 nondet!(/** bounded top-level optional so deterministic */),
1178 );
1179 Optional::new(tick.clone(), inner.ir_node.replace(HydroNode::Placeholder))
1180 }
1181
1182 /// Converts this optional into a [`Stream`] containing a single element, the value, if it is
1183 /// non-null. Otherwise, the stream is empty.
1184 ///
1185 /// # Example
1186 /// ```rust
1187 /// # #[cfg(feature = "deploy")] {
1188 /// # use hydro_lang::prelude::*;
1189 /// # use futures::StreamExt;
1190 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1191 /// # let tick = process.tick();
1192 /// # // ticks are lazy by default, forces the second tick to run
1193 /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1194 /// # let batch_first_tick = process
1195 /// # .source_iter(q!(vec![]))
1196 /// # .batch(&tick, nondet!(/** test */));
1197 /// # let batch_second_tick = process
1198 /// # .source_iter(q!(vec![123, 456]))
1199 /// # .batch(&tick, nondet!(/** test */))
1200 /// # .defer_tick(); // appears on the second tick
1201 /// # let input_batch = batch_first_tick.chain(batch_second_tick);
1202 /// input_batch // first tick: [], second tick: [123, 456]
1203 /// .clone()
1204 /// .max()
1205 /// .into_stream()
1206 /// .chain(input_batch)
1207 /// .all_ticks()
1208 /// # }, |mut stream| async move {
1209 /// // [456, 123, 456]
1210 /// # for w in vec![456, 123, 456] {
1211 /// # assert_eq!(stream.next().await.unwrap(), w);
1212 /// # }
1213 /// # }));
1214 /// # }
1215 /// ```
1216 pub fn into_stream(self) -> Stream<T, L, Bounded, TotalOrder, ExactlyOnce>
1217 where
1218 B: IsBounded,
1219 {
1220 Stream::new(
1221 self.location.clone(),
1222 HydroNode::Cast {
1223 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1224 metadata: self.location.new_node_metadata(Stream::<
1225 T,
1226 Tick<L>,
1227 Bounded,
1228 TotalOrder,
1229 ExactlyOnce,
1230 >::collection_kind()),
1231 },
1232 )
1233 }
1234
1235 /// Filters this optional, passing through the value if the boolean signal is `true`,
1236 /// otherwise the output is null.
1237 ///
1238 /// # Example
1239 /// ```rust
1240 /// # #[cfg(feature = "deploy")] {
1241 /// # use hydro_lang::prelude::*;
1242 /// # use futures::StreamExt;
1243 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1244 /// let tick = process.tick();
1245 /// // ticks are lazy by default, forces the second tick to run
1246 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1247 ///
1248 /// let some_first_tick = tick.optional_first_tick(q!(()));
1249 /// let signal = some_first_tick.is_some(); // true on first tick, false on second
1250 /// let batch_first_tick = process
1251 /// .source_iter(q!(vec![456]))
1252 /// .batch(&tick, nondet!(/** test */));
1253 /// let batch_second_tick = process
1254 /// .source_iter(q!(vec![789]))
1255 /// .batch(&tick, nondet!(/** test */))
1256 /// .defer_tick();
1257 /// batch_first_tick.chain(batch_second_tick).first()
1258 /// .filter_if(signal)
1259 /// .unwrap_or(tick.singleton(q!(0)))
1260 /// .all_ticks()
1261 /// # }, |mut stream| async move {
1262 /// // [456, 0]
1263 /// # for w in vec![456, 0] {
1264 /// # assert_eq!(stream.next().await.unwrap(), w);
1265 /// # }
1266 /// # }));
1267 /// # }
1268 /// ```
1269 pub fn filter_if(self, signal: Singleton<bool, L, B>) -> Optional<T, L, B>
1270 where
1271 B: IsBounded,
1272 {
1273 self.zip(signal.filter(q!(|b| *b))).map(q!(|(d, _)| d))
1274 }
1275
1276 /// Filters this optional, passing through the optional value if it is non-null **and** the
1277 /// argument (a [`Bounded`] [`Optional`]) is non-null, otherwise the output is null.
1278 ///
1279 /// Useful for conditionally processing, such as only emitting an optional's value outside
1280 /// a tick if some other condition is satisfied.
1281 ///
1282 /// # Example
1283 /// ```rust
1284 /// # #[cfg(feature = "deploy")] {
1285 /// # use hydro_lang::prelude::*;
1286 /// # use futures::StreamExt;
1287 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1288 /// let tick = process.tick();
1289 /// // ticks are lazy by default, forces the second tick to run
1290 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1291 ///
1292 /// let batch_first_tick = process
1293 /// .source_iter(q!(vec![]))
1294 /// .batch(&tick, nondet!(/** test */));
1295 /// let batch_second_tick = process
1296 /// .source_iter(q!(vec![456]))
1297 /// .batch(&tick, nondet!(/** test */))
1298 /// .defer_tick(); // appears on the second tick
1299 /// let some_on_first_tick = tick.optional_first_tick(q!(()));
1300 /// batch_first_tick.chain(batch_second_tick).first()
1301 /// .filter_if_some(some_on_first_tick)
1302 /// .unwrap_or(tick.singleton(q!(789)))
1303 /// .all_ticks()
1304 /// # }, |mut stream| async move {
1305 /// // [789, 789]
1306 /// # for w in vec![789, 789] {
1307 /// # assert_eq!(stream.next().await.unwrap(), w);
1308 /// # }
1309 /// # }));
1310 /// # }
1311 /// ```
1312 #[deprecated(note = "use `filter_if` with `Optional::is_some()` instead")]
1313 pub fn filter_if_some<U>(self, signal: Optional<U, L, B>) -> Optional<T, L, B>
1314 where
1315 B: IsBounded,
1316 {
1317 self.filter_if(signal.is_some())
1318 }
1319
1320 /// Filters this optional, passing through the optional value if it is non-null **and** the
1321 /// argument (a [`Bounded`] [`Optional`]) is _null_, otherwise the output is null.
1322 ///
1323 /// Useful for conditionally processing, such as only emitting an optional's value outside
1324 /// a tick if some other condition is satisfied.
1325 ///
1326 /// # Example
1327 /// ```rust
1328 /// # #[cfg(feature = "deploy")] {
1329 /// # use hydro_lang::prelude::*;
1330 /// # use futures::StreamExt;
1331 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1332 /// let tick = process.tick();
1333 /// // ticks are lazy by default, forces the second tick to run
1334 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1335 ///
1336 /// let batch_first_tick = process
1337 /// .source_iter(q!(vec![]))
1338 /// .batch(&tick, nondet!(/** test */));
1339 /// let batch_second_tick = process
1340 /// .source_iter(q!(vec![456]))
1341 /// .batch(&tick, nondet!(/** test */))
1342 /// .defer_tick(); // appears on the second tick
1343 /// let some_on_first_tick = tick.optional_first_tick(q!(()));
1344 /// batch_first_tick.chain(batch_second_tick).first()
1345 /// .filter_if_none(some_on_first_tick)
1346 /// .unwrap_or(tick.singleton(q!(789)))
1347 /// .all_ticks()
1348 /// # }, |mut stream| async move {
1349 /// // [789, 789]
1350 /// # for w in vec![789, 456] {
1351 /// # assert_eq!(stream.next().await.unwrap(), w);
1352 /// # }
1353 /// # }));
1354 /// # }
1355 /// ```
1356 #[deprecated(note = "use `filter_if` with `!Optional::is_some()` instead")]
1357 pub fn filter_if_none<U>(self, other: Optional<U, L, B>) -> Optional<T, L, B>
1358 where
1359 B: IsBounded,
1360 {
1361 self.filter_if(other.is_none())
1362 }
1363
1364 /// If `self` is null, emits a null optional, but if it non-null, emits `value`.
1365 ///
1366 /// Useful for gating the release of a [`Singleton`] on a condition of the [`Optional`]
1367 /// having a value, such as only releasing a piece of state if the node is the leader.
1368 ///
1369 /// # Example
1370 /// ```rust
1371 /// # #[cfg(feature = "deploy")] {
1372 /// # use hydro_lang::prelude::*;
1373 /// # use futures::StreamExt;
1374 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1375 /// let tick = process.tick();
1376 /// // ticks are lazy by default, forces the second tick to run
1377 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1378 ///
1379 /// let some_on_first_tick = tick.optional_first_tick(q!(()));
1380 /// some_on_first_tick
1381 /// .if_some_then(tick.singleton(q!(456)))
1382 /// .unwrap_or(tick.singleton(q!(123)))
1383 /// # .all_ticks()
1384 /// # }, |mut stream| async move {
1385 /// // 456 (first tick) ~> 123 (second tick onwards)
1386 /// # for w in vec![456, 123, 123] {
1387 /// # assert_eq!(stream.next().await.unwrap(), w);
1388 /// # }
1389 /// # }));
1390 /// # }
1391 /// ```
1392 #[deprecated(note = "use `filter_if` with `Optional::is_some()` instead")]
1393 pub fn if_some_then<U>(self, value: Singleton<U, L, B>) -> Optional<U, L, B>
1394 where
1395 B: IsBounded,
1396 {
1397 value.filter_if(self.is_some())
1398 }
1399}
1400
1401impl<'a, K, V, L, B: OptionalBound> Optional<(K, V), L, B>
1402where
1403 L: Location<'a>,
1404{
1405 /// Converts this optional into a [`KeyedSingleton`] containing a single entry with the
1406 /// key-value pair of this [`Optional`].
1407 ///
1408 /// If this [`Optional`] is [`Bounded`], the [`KeyedSingleton`] will be [`Bounded`] as well
1409 /// if it is [`Unbounded`], the [`KeyedSingleton`] will be [`Unbounded`], which means that
1410 /// the entry will be updated and appear / disappear according to the state of the
1411 /// [`Optional`].
1412 ///
1413 /// The `K: Clone + PartialEq` bounds are needed because when the optional is [`Unbounded`]
1414 /// at a top-level location, the compiled representation tracks the previously emitted key
1415 /// so the entry can be removed when the optional becomes null or moves to a different key.
1416 pub fn into_keyed_singleton(self) -> KeyedSingleton<K, V, L, B::UnderlyingBound>
1417 where
1418 K: Clone + PartialEq,
1419 {
1420 KeyedSingleton::new(
1421 self.location.clone(),
1422 HydroNode::Cast {
1423 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1424 metadata: self.location.new_node_metadata(KeyedSingleton::<
1425 K,
1426 V,
1427 L,
1428 B::UnderlyingBound,
1429 >::collection_kind()),
1430 },
1431 )
1432 }
1433}
1434
1435impl<'a, T, L, B: OptionalBound> Optional<T, Atomic<L>, B>
1436where
1437 L: Location<'a>,
1438{
1439 /// Returns an optional value corresponding to the latest snapshot of the optional
1440 /// being atomically processed. The snapshot at tick `t + 1` is guaranteed to include
1441 /// at least all relevant data that contributed to the snapshot at tick `t`. Furthermore,
1442 /// all snapshots of this optional into the atomic-associated tick will observe the
1443 /// same value each tick.
1444 ///
1445 /// # Non-Determinism
1446 /// Because this picks a snapshot of a optional whose value is continuously changing,
1447 /// the output optional has a non-deterministic value since the snapshot can be at an
1448 /// arbitrary point in time.
1449 pub fn snapshot_atomic<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1450 self,
1451 tick: &Tick<L2>,
1452 _nondet: NonDet,
1453 ) -> Optional<T, Tick<L::DropConsistency>, Bounded> {
1454 assert_eq!(
1455 Location::id(tick.parent_location()),
1456 Location::id(self.location.tick.parent_location())
1457 );
1458 Optional::new(
1459 tick.drop_consistency(),
1460 HydroNode::Snapshot {
1461 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1462 metadata: tick
1463 .new_node_metadata(Optional::<T, Tick<L>, Bounded>::collection_kind()),
1464 },
1465 )
1466 }
1467}
1468
1469impl<'a, T, L, B: OptionalBound> Optional<T, L, B>
1470where
1471 L: Location<'a>,
1472{
1473 /// Given a tick, returns a optional value corresponding to a snapshot of the optional
1474 /// as of that tick. The snapshot at tick `t + 1` is guaranteed to include at least all
1475 /// relevant data that contributed to the snapshot at tick `t`.
1476 ///
1477 /// # Non-Determinism
1478 /// Because this picks a snapshot of a optional whose value is continuously changing,
1479 /// the output optional has a non-deterministic value since the snapshot can be at an
1480 /// arbitrary point in time.
1481 pub fn snapshot<L2: Location<'a, DropConsistency = L::DropConsistency>>(
1482 self,
1483 tick: &Tick<L2>,
1484 _nondet: NonDet,
1485 ) -> Optional<T, Tick<L::DropConsistency>, Bounded> {
1486 assert_eq!(
1487 Location::id(tick.parent_location()),
1488 Location::id(&self.location)
1489 );
1490 Optional::new(
1491 tick.drop_consistency(),
1492 HydroNode::Snapshot {
1493 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1494 metadata: tick
1495 .new_node_metadata(Optional::<T, Tick<L>, Bounded>::collection_kind()),
1496 },
1497 )
1498 }
1499
1500 /// Eagerly samples the optional as fast as possible, returning a stream of snapshots
1501 /// with order corresponding to increasing prefixes of data contributing to the optional.
1502 ///
1503 /// This requires the optional to be null-only-initially ([`IsInitNone`]): a general
1504 /// [`Unbounded`] optional can *become null again*, which a `Stream<T>` of samples cannot
1505 /// represent (dropping the null transitions would leave stale values observable). To
1506 /// sample an [`Unbounded`] optional, use [`Optional::into_singleton`] first to observe
1507 /// `Option<T>` snapshots (including `None`).
1508 ///
1509 /// # Non-Determinism
1510 /// At runtime, the optional will be arbitrarily sampled as fast as possible, but due
1511 /// to non-deterministic batching and arrival of inputs, the output stream is
1512 /// non-deterministic.
1513 pub fn sample_eager(
1514 self,
1515 nondet: NonDet,
1516 ) -> Stream<T, L::DropConsistency, Unbounded, TotalOrder, AtLeastOnce>
1517 where
1518 B: IsInitNone,
1519 {
1520 let tick = self.location.tick();
1521 self.snapshot(&tick, nondet).all_ticks().weaken_retries()
1522 }
1523
1524 /// Given a time interval, returns a stream corresponding to snapshots of the optional
1525 /// value taken at various points in time. Because the input optional may be
1526 /// [`Unbounded`], there are no guarantees on what these snapshots are other than they
1527 /// represent the value of the optional given some prefix of the streams leading up to
1528 /// it.
1529 ///
1530 /// # Non-Determinism
1531 /// The output stream is non-deterministic in which elements are sampled, since this
1532 /// is controlled by a clock.
1533 #[cfg(feature = "tokio")]
1534 pub fn sample_every(
1535 self,
1536 interval: impl QuotedWithContext<'a, std::time::Duration, L> + Copy + 'a,
1537 nondet: NonDet,
1538 ) -> Stream<T, L::DropConsistency, Unbounded, TotalOrder, AtLeastOnce>
1539 where
1540 L: TopLevel<'a>,
1541 {
1542 let samples = self.location.source_interval(interval);
1543 let tick = self.location.tick();
1544
1545 self.snapshot(&tick, nondet)
1546 .filter_if(
1547 samples
1548 .batch(
1549 &tick,
1550 nondet!(
1551 /// sample timing is captured by the caller's guard
1552 nondet
1553 ),
1554 )
1555 .first()
1556 .is_some(),
1557 )
1558 .all_ticks()
1559 .weaken_retries()
1560 }
1561}
1562
1563impl<'a, T, L> Optional<T, Tick<L>, Bounded>
1564where
1565 L: Location<'a>,
1566{
1567 /// Asynchronously yields the value of this singleton outside the tick as an unbounded stream,
1568 /// which will stream the value computed in _each_ tick as a separate stream element (skipping
1569 /// null values).
1570 ///
1571 /// Unlike [`Optional::latest`], the value computed in each tick is emitted separately,
1572 /// producing one element in the output for each (non-null) tick. This is useful for batched
1573 /// computations, where the results from each tick must be combined together.
1574 ///
1575 /// # Example
1576 /// ```rust
1577 /// # #[cfg(feature = "deploy")] {
1578 /// # use hydro_lang::prelude::*;
1579 /// # use futures::StreamExt;
1580 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1581 /// # let tick = process.tick();
1582 /// # // ticks are lazy by default, forces the second tick to run
1583 /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1584 /// # let batch_first_tick = process
1585 /// # .source_iter(q!(vec![]))
1586 /// # .batch(&tick, nondet!(/** test */));
1587 /// # let batch_second_tick = process
1588 /// # .source_iter(q!(vec![1, 2, 3]))
1589 /// # .batch(&tick, nondet!(/** test */))
1590 /// # .defer_tick(); // appears on the second tick
1591 /// # let input_batch = batch_first_tick.chain(batch_second_tick);
1592 /// input_batch // first tick: [], second tick: [1, 2, 3]
1593 /// .max()
1594 /// .all_ticks()
1595 /// # }, |mut stream| async move {
1596 /// // [3]
1597 /// # for w in vec![3] {
1598 /// # assert_eq!(stream.next().await.unwrap(), w);
1599 /// # }
1600 /// # }));
1601 /// # }
1602 /// ```
1603 pub fn all_ticks(self) -> Stream<T, L, Unbounded, TotalOrder, ExactlyOnce> {
1604 self.into_stream().all_ticks()
1605 }
1606
1607 /// Synchronously yields the value of this optional outside the tick as an unbounded stream,
1608 /// which will stream the value computed in _each_ tick as a separate stream element.
1609 ///
1610 /// Unlike [`Optional::all_ticks`], this preserves synchronous execution, as the output stream
1611 /// is emitted in an [`Atomic`] context that will process elements synchronously with the input
1612 /// optional's [`Tick`] context.
1613 pub fn all_ticks_atomic(self) -> Stream<T, Atomic<L>, Unbounded, TotalOrder, ExactlyOnce> {
1614 self.into_stream().all_ticks_atomic()
1615 }
1616
1617 /// Asynchronously yields this optional outside the tick as an unbounded optional, which will
1618 /// be asynchronously updated with the latest value of the optional inside the tick, including
1619 /// whether the optional is null or not.
1620 ///
1621 /// This converts a bounded value _inside_ a tick into an asynchronous value outside the
1622 /// tick that tracks the inner value. This is useful for getting the value as of the
1623 /// "most recent" tick, but note that updates are propagated asynchronously outside the tick.
1624 ///
1625 /// # Example
1626 /// ```rust
1627 /// # #[cfg(feature = "deploy")] {
1628 /// # use hydro_lang::prelude::*;
1629 /// # use futures::StreamExt;
1630 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1631 /// # let tick = process.tick();
1632 /// # // ticks are lazy by default, forces the second tick to run
1633 /// # tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1634 /// # let batch_first_tick = process
1635 /// # .source_iter(q!(vec![]))
1636 /// # .batch(&tick, nondet!(/** test */));
1637 /// # let batch_second_tick = process
1638 /// # .source_iter(q!(vec![1, 2, 3]))
1639 /// # .batch(&tick, nondet!(/** test */))
1640 /// # .defer_tick(); // appears on the second tick
1641 /// # let input_batch = batch_first_tick.chain(batch_second_tick);
1642 /// input_batch // first tick: [], second tick: [1, 2, 3]
1643 /// .max()
1644 /// .latest()
1645 /// # .into_singleton()
1646 /// # .sample_eager(nondet!(/** test */))
1647 /// # }, |mut stream| async move {
1648 /// // asynchronously changes from None ~> 3
1649 /// # for w in vec![None, Some(3)] {
1650 /// # assert_eq!(stream.next().await.unwrap(), w);
1651 /// # }
1652 /// # }));
1653 /// # }
1654 /// ```
1655 pub fn latest(self) -> Optional<T, L, Unbounded> {
1656 Optional::new(
1657 self.location.parent_location().clone(),
1658 HydroNode::YieldConcat {
1659 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1660 metadata: self
1661 .location
1662 .parent_location()
1663 .new_node_metadata(Optional::<T, L, Unbounded>::collection_kind()),
1664 },
1665 )
1666 }
1667
1668 /// Synchronously yields this optional outside the tick as an unbounded optional, which will
1669 /// be updated with the latest value of the optional inside the tick.
1670 ///
1671 /// Unlike [`Optional::latest`], this preserves synchronous execution, as the output optional
1672 /// is emitted in an [`Atomic`] context that will process elements synchronously with the input
1673 /// optional's [`Tick`] context.
1674 pub fn latest_atomic(self) -> Optional<T, Atomic<L>, Unbounded> {
1675 let out_location = Atomic {
1676 tick: self.location.clone(),
1677 };
1678
1679 Optional::new(
1680 out_location.clone(),
1681 HydroNode::YieldConcat {
1682 inner: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1683 metadata: out_location
1684 .new_node_metadata(Optional::<T, Atomic<L>, Unbounded>::collection_kind()),
1685 },
1686 )
1687 }
1688
1689 /// Shifts the state in `self` to the **next tick**, so that the returned optional at tick `T`
1690 /// always has the state of `self` at tick `T - 1`.
1691 ///
1692 /// At tick `0`, the output optional is null, since there is no previous tick.
1693 ///
1694 /// This operator enables stateful iterative processing with ticks, by sending data from one
1695 /// tick to the next. For example, you can use it to compare state across consecutive batches.
1696 ///
1697 /// # Example
1698 /// ```rust
1699 /// # #[cfg(feature = "deploy")] {
1700 /// # use hydro_lang::prelude::*;
1701 /// # use futures::StreamExt;
1702 /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1703 /// let tick = process.tick();
1704 /// // ticks are lazy by default, forces the second tick to run
1705 /// tick.spin_batch(q!(1)).all_ticks().for_each(q!(|_| {}));
1706 ///
1707 /// let batch_first_tick = process
1708 /// .source_iter(q!(vec![1, 2]))
1709 /// .batch(&tick, nondet!(/** test */));
1710 /// let batch_second_tick = process
1711 /// .source_iter(q!(vec![3, 4]))
1712 /// .batch(&tick, nondet!(/** test */))
1713 /// .defer_tick(); // appears on the second tick
1714 /// let current_tick_sum = batch_first_tick.chain(batch_second_tick)
1715 /// .reduce(q!(|state, v| *state += v));
1716 ///
1717 /// current_tick_sum.clone().into_singleton().zip(
1718 /// current_tick_sum.defer_tick().into_singleton() // state from previous tick
1719 /// ).all_ticks()
1720 /// # }, |mut stream| async move {
1721 /// // [(Some(3), None) /* first tick */, (Some(7), Some(3)) /* second tick */]
1722 /// # for w in vec![(Some(3), None), (Some(7), Some(3))] {
1723 /// # assert_eq!(stream.next().await.unwrap(), w);
1724 /// # }
1725 /// # }));
1726 /// # }
1727 /// ```
1728 pub fn defer_tick(self) -> Optional<T, Tick<L>, Bounded> {
1729 Optional::new(
1730 self.location.clone(),
1731 HydroNode::DeferTick {
1732 input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1733 metadata: self.location.new_node_metadata(Self::collection_kind()),
1734 },
1735 )
1736 }
1737}
1738
1739#[cfg(test)]
1740mod tests {
1741 #[cfg(feature = "deploy")]
1742 use futures::StreamExt;
1743 #[cfg(feature = "deploy")]
1744 use hydro_deploy::Deployment;
1745 #[cfg(any(feature = "deploy", feature = "sim"))]
1746 use stageleft::q;
1747
1748 #[cfg(feature = "deploy")]
1749 use super::Optional;
1750 #[cfg(any(feature = "deploy", feature = "sim"))]
1751 use crate::compile::builder::FlowBuilder;
1752 #[cfg(any(feature = "deploy", feature = "sim"))]
1753 use crate::location::Location;
1754 #[cfg(feature = "deploy")]
1755 use crate::nondet::nondet;
1756
1757 #[cfg(feature = "deploy")]
1758 #[tokio::test]
1759 async fn optional_or_cardinality() {
1760 let mut deployment = Deployment::new();
1761
1762 let mut flow = FlowBuilder::new();
1763 let node = flow.process::<()>();
1764 let external = flow.external::<()>();
1765
1766 let node_tick = node.tick();
1767 let tick_singleton = node_tick.singleton(q!(123));
1768 let tick_optional_inhabited: Optional<_, _, _> = tick_singleton.into();
1769 let counts = tick_optional_inhabited
1770 .clone()
1771 .or(tick_optional_inhabited)
1772 .into_stream()
1773 .count()
1774 .all_ticks()
1775 .send_bincode_external(&external);
1776
1777 let nodes = flow
1778 .with_process(&node, deployment.Localhost())
1779 .with_external(&external, deployment.Localhost())
1780 .deploy(&mut deployment);
1781
1782 deployment.deploy().await.unwrap();
1783
1784 let mut external_out = nodes.connect(counts).await;
1785
1786 deployment.start().await.unwrap();
1787
1788 assert_eq!(external_out.next().await.unwrap(), 1);
1789 }
1790
1791 #[cfg(feature = "deploy")]
1792 #[tokio::test]
1793 async fn into_singleton_top_level_none_cardinality() {
1794 let mut deployment = Deployment::new();
1795
1796 let mut flow = FlowBuilder::new();
1797 let node = flow.process::<()>();
1798 let external = flow.external::<()>();
1799
1800 let node_tick = node.tick();
1801 let top_level_none = node.singleton(q!(123)).filter(q!(|_| false));
1802 let into_singleton = top_level_none.into_singleton();
1803
1804 let tick_driver = node.spin();
1805
1806 let counts = into_singleton
1807 .snapshot(&node_tick, nondet!(/** test */))
1808 .into_stream()
1809 .count()
1810 .zip(tick_driver.batch(&node_tick, nondet!(/** test */)).count())
1811 .map(q!(|(c, _)| c))
1812 .all_ticks()
1813 .send_bincode_external(&external);
1814
1815 let nodes = flow
1816 .with_process(&node, deployment.Localhost())
1817 .with_external(&external, deployment.Localhost())
1818 .deploy(&mut deployment);
1819
1820 deployment.deploy().await.unwrap();
1821
1822 let mut external_out = nodes.connect(counts).await;
1823
1824 deployment.start().await.unwrap();
1825
1826 assert_eq!(external_out.next().await.unwrap(), 1);
1827 assert_eq!(external_out.next().await.unwrap(), 1);
1828 assert_eq!(external_out.next().await.unwrap(), 1);
1829 }
1830
1831 #[cfg(feature = "deploy")]
1832 #[tokio::test]
1833 async fn into_singleton_unbounded_top_level_none_cardinality() {
1834 let mut deployment = Deployment::new();
1835
1836 let mut flow = FlowBuilder::new();
1837 let node = flow.process::<()>();
1838 let external = flow.external::<()>();
1839
1840 let top_level_none = node
1841 .tick()
1842 .singleton(q!(123))
1843 .latest()
1844 .filter(q!(|_| false));
1845 let into_singleton = top_level_none.into_singleton();
1846
1847 let tick_driver = node.spin();
1848
1849 let tick_later = node.tick();
1850 let counts = into_singleton
1851 .snapshot(&tick_later, nondet!(/** test */))
1852 .into_stream()
1853 .count()
1854 .zip(tick_driver.batch(&tick_later, nondet!(/** test */)).count())
1855 .map(q!(|(c, _)| c))
1856 .all_ticks()
1857 .send_bincode_external(&external);
1858
1859 let nodes = flow
1860 .with_process(&node, deployment.Localhost())
1861 .with_external(&external, deployment.Localhost())
1862 .deploy(&mut deployment);
1863
1864 deployment.deploy().await.unwrap();
1865
1866 let mut external_out = nodes.connect(counts).await;
1867
1868 deployment.start().await.unwrap();
1869
1870 assert_eq!(external_out.next().await.unwrap(), 1);
1871 assert_eq!(external_out.next().await.unwrap(), 1);
1872 assert_eq!(external_out.next().await.unwrap(), 1);
1873 }
1874
1875 #[cfg(feature = "sim")]
1876 #[test]
1877 fn top_level_optional_some_into_stream_no_replay() {
1878 let mut flow = FlowBuilder::new();
1879 let node = flow.process::<()>();
1880
1881 let source_iter = node.source_iter(q!(vec![1, 2, 3, 4]));
1882 let folded = source_iter.fold(q!(|| 0), q!(|a, b| *a += b));
1883 let filtered_some = folded.filter(q!(|_| true));
1884
1885 let out_recv = filtered_some.into_stream().sim_output();
1886
1887 flow.sim().exhaustive(async || {
1888 out_recv.assert_yields_only([10]).await;
1889 });
1890 }
1891
1892 #[cfg(feature = "sim")]
1893 #[test]
1894 fn top_level_optional_none_into_stream_no_replay() {
1895 let mut flow = FlowBuilder::new();
1896 let node = flow.process::<()>();
1897
1898 let source_iter = node.source_iter(q!(vec![1, 2, 3, 4]));
1899 let folded = source_iter.fold(q!(|| 0), q!(|a, b| *a += b));
1900 let filtered_none = folded.filter(q!(|_| false));
1901
1902 let out_recv = filtered_none.into_stream().sim_output();
1903
1904 flow.sim().exhaustive(async || {
1905 out_recv.assert_yields_only([] as [i32; 0]).await;
1906 });
1907 }
1908
1909 #[cfg(feature = "deploy")]
1910 #[tokio::test]
1911 async fn test_optional_ref() {
1912 let mut deployment = Deployment::new();
1913
1914 let mut flow = FlowBuilder::new();
1915 let external = flow.external::<()>();
1916 let p1 = flow.process::<()>();
1917
1918 // Create an optional: reduce 0..5 => Some(10) (sum via reduce)
1919 let my_opt = p1.source_iter(q!(0..5i32)).reduce(q!(|a, b| *a += b));
1920
1921 let opt_ref = my_opt.by_ref();
1922
1923 // Use the optional ref in a map: unwrap_or(0) + x
1924 let out_port = p1
1925 .source_iter(q!(1..=3i32))
1926 .map(q!(|x| x + opt_ref.unwrap_or(0)))
1927 .send_bincode_external(&external);
1928
1929 let nodes = flow
1930 .with_default_optimize()
1931 .with_process(&p1, deployment.Localhost())
1932 .with_external(&external, deployment.Localhost())
1933 .deploy(&mut deployment);
1934
1935 deployment.deploy().await.unwrap();
1936
1937 let mut out_recv = nodes.connect(out_port).await;
1938
1939 deployment.start().await.unwrap();
1940
1941 let mut results = Vec::new();
1942 for _ in 0..3 {
1943 results.push(out_recv.next().await.unwrap());
1944 }
1945 results.sort();
1946 // reduce(0..5) = 10, so results should be 11, 12, 13
1947 assert_eq!(results, vec![11, 12, 13]);
1948 }
1949
1950 #[cfg(feature = "deploy")]
1951 #[tokio::test]
1952 async fn test_optional_ref_none() {
1953 let mut deployment = Deployment::new();
1954
1955 let mut flow = FlowBuilder::new();
1956 let external = flow.external::<()>();
1957 let p1 = flow.process::<()>();
1958
1959 // Create an optional from an empty source => None
1960 let my_opt = p1
1961 .source_iter(q!(std::iter::empty::<i32>()))
1962 .reduce(q!(|a, b| *a += b));
1963
1964 let opt_ref = my_opt.by_ref();
1965
1966 // Use the optional ref: should be None, so unwrap_or(99)
1967 let out_port = p1
1968 .source_iter(q!(1..=2i32))
1969 .map(q!(|x| x + opt_ref.unwrap_or(99)))
1970 .send_bincode_external(&external);
1971
1972 let nodes = flow
1973 .with_default_optimize()
1974 .with_process(&p1, deployment.Localhost())
1975 .with_external(&external, deployment.Localhost())
1976 .deploy(&mut deployment);
1977
1978 deployment.deploy().await.unwrap();
1979
1980 let mut out_recv = nodes.connect(out_port).await;
1981
1982 deployment.start().await.unwrap();
1983
1984 let mut results = Vec::new();
1985 for _ in 0..2 {
1986 results.push(out_recv.next().await.unwrap());
1987 }
1988 results.sort();
1989 // optional is None, so unwrap_or(99) => 100, 101
1990 assert_eq!(results, vec![100, 101]);
1991 }
1992
1993 #[cfg(feature = "deploy")]
1994 #[tokio::test]
1995 async fn test_optional_ref_and_consume() {
1996 let mut deployment = Deployment::new();
1997
1998 let mut flow = FlowBuilder::new();
1999 let external = flow.external::<()>();
2000 let p1 = flow.process::<()>();
2001
2002 // Use reduce to produce an Optional
2003 let my_opt = p1.source_iter(q!(0..5i32)).reduce(q!(|a, b| *a += b));
2004
2005 let opt_ref = my_opt.by_ref();
2006
2007 // Reference path
2008 let out_port_ref = p1
2009 .source_iter(q!(1..=2i32))
2010 .map(q!(|x| x + opt_ref.unwrap_or(0)))
2011 .send_bincode_external(&external);
2012
2013 let nodes = flow
2014 .with_default_optimize()
2015 .with_process(&p1, deployment.Localhost())
2016 .with_external(&external, deployment.Localhost())
2017 .deploy(&mut deployment);
2018
2019 deployment.deploy().await.unwrap();
2020
2021 let mut out_recv_ref = nodes.connect(out_port_ref).await;
2022
2023 deployment.start().await.unwrap();
2024
2025 let mut ref_results = Vec::new();
2026 for _ in 0..2 {
2027 ref_results.push(out_recv_ref.next().await.unwrap());
2028 }
2029 ref_results.sort();
2030 // reduce(0..5) = 10, so 1+10=11, 2+10=12
2031 assert_eq!(ref_results, vec![11, 12]);
2032 }
2033}