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