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