Skip to main content

hydro_lang/
handoff_ref.rs

1//! Reference handles for capturing singletons, optionals, and streams in `q!()` closures.
2//!
3//! Each handle type wraps a `&RefCell<HydroNode>` and, when captured inside a `q!()` closure,
4//! registers itself with the current capture scope. At codegen time, the IR node is lowered
5//! to the corresponding DFIR pseudo-operator (`singleton()`, `optional()`, or `handoff()`),
6//! and the reference resolves to the appropriate borrow type.
7//!
8//! Each handle tracks the [`Location`] **and** the boundedness of the collection it refers to
9//! (see [`OperatorContext`]). A handle can only be captured inside closures passed to operators
10//! on collections with a matching location and boundedness. This is required for soundness:
11//! a bounded collection is only materialized on the first tick, while closures on unbounded
12//! collections continue to run on later ticks, where the referenced value no longer exists and
13//! accessing it would crash at runtime.
14
15use std::cell::RefCell;
16use std::marker::PhantomData;
17use std::rc::Rc;
18
19use proc_macro2::Span;
20use quote::quote;
21use stageleft::runtime_support::{FreeVariableWithContextWithProps, QuoteTokens};
22
23use crate::compile::ir::{AccessCounter, HydroNode, SharedNode};
24use crate::live_collections::OperatorContext;
25use crate::location::Location;
26
27/// Determines which DFIR pseudo-operator a reference node lowers to.
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
29pub enum HandoffRefKind {
30    /// `-> singleton()` — exactly one item, `#var` gives `&T`.
31    Singleton,
32    /// `-> optional()` — zero or one item, `#var` gives `&Option<T>`.
33    Optional,
34    /// `-> handoff()` — zero or more items, `#var` gives `&Vec<T>`.
35    Vec,
36}
37
38// Thread-local storage for handoff references captured during `q!()` expansion.
39// Stores the `HydroNode::Reference` and `is_mut: bool` for each reference captured in the current closure.
40// The index determines the ident name via `handoff_ref_ident`.
41thread_local! {
42    static CAPTURED_REFS: RefCell<Option<Vec<(HydroNode, bool)>>> = const { RefCell::new(None) };
43}
44
45/// Returns the canonical ident for a captured ref at the given index within a closure.
46pub(crate) fn handoff_ref_ident(index: usize) -> syn::Ident {
47    syn::Ident::new(
48        &format!("__hydro_singleton_ref_{}", index),
49        Span::call_site(),
50    )
51}
52
53/// Activate the reference capture context. Must be called before `q!()` expansion
54/// that may capture handoff references. Returns a `ClosureExpr` bundling the expression with any
55/// captured references.
56pub fn with_ref_capture(
57    f: impl FnOnce() -> crate::compile::ir::DebugExpr,
58) -> crate::compile::ir::ClosureExpr {
59    CAPTURED_REFS.with(|cell| {
60        let prev = cell.borrow_mut().replace(Vec::new());
61        assert!(
62            prev.is_none(),
63            "nested handoff reference capture scopes are not supported"
64        );
65    });
66    let expr = (f)();
67    let captured_refs = CAPTURED_REFS.with(|cell| cell.borrow_mut().take().unwrap());
68    crate::compile::ir::ClosureExpr::new(expr, captured_refs)
69}
70
71/// Shared registration logic: wraps the IR node in `HydroNode::Reference` if needed,
72/// pushes it to the capture list, and returns the ident to use in the closure body.
73fn register_handoff_ref(
74    ir_node: &RefCell<HydroNode>,
75    is_mut: bool,
76    kind: HandoffRefKind,
77) -> syn::Ident {
78    CAPTURED_REFS.with(|cell| {
79        let mut guard = cell.borrow_mut();
80        let refs = guard.as_mut().expect(
81            "HandoffRef used inside q!() but no reference capture scope is active. \
82             This is a bug — reference capture should be set up by the operator that uses q!().",
83        );
84
85        let index = refs.len();
86        let ident = handoff_ref_ident(index);
87
88        let metadata = ir_node.borrow().metadata().clone();
89
90        // Wrap in HydroNode::Reference for materialization + identity tracking.
91        // If already a Reference node, reuse it.
92        if !matches!(&*ir_node.borrow(), HydroNode::Reference { .. }) {
93            let orig = ir_node.replace(HydroNode::Placeholder);
94            *ir_node.borrow_mut() = HydroNode::Reference {
95                inner: SharedNode(Rc::new(RefCell::new(orig))),
96                kind,
97                access_counter: AccessCounter::new(),
98                metadata: metadata.clone(),
99            };
100        }
101
102        let borrow: std::cell::Ref<'_, HydroNode> = ir_node.borrow();
103        let HydroNode::Reference {
104            inner,
105            access_counter,
106            ..
107        } = &*borrow
108        else {
109            unreachable!()
110        };
111
112        // Compute access group at staging time (code order).
113        let group = access_counter.next_group(is_mut);
114
115        refs.push((
116            HydroNode::Reference {
117                inner: SharedNode(Rc::clone(&inner.0)),
118                kind,
119                access_counter: group,
120                metadata,
121            },
122            is_mut,
123        ));
124
125        ident
126    })
127}
128
129/// Macro to define a handoff reference struct with all necessary trait impls.
130macro_rules! define_handoff_ref {
131    (
132        $(
133            $(#[$meta:meta])*
134            $name:ident, $is_mut:expr, $kind:expr, $output:ty
135        )+
136    ) => {
137        $(
138            $(#[$meta])*
139            pub struct $name<'a, 'slf, T, L, B> {
140                pub(crate) ir_node: &'slf RefCell<HydroNode>,
141                _phantom: PhantomData<(&'a T, L, B)>,
142            }
143
144            impl<'slf, T, L, B> $name<'_, 'slf, T, L, B> {
145                /// Creates a new reference handle from an IR node cell.
146                pub(crate) fn new(ir_node: &'slf RefCell<HydroNode>) -> Self {
147                    Self {
148                        ir_node,
149                        _phantom: PhantomData,
150                    }
151                }
152            }
153
154            impl<T, L, B> Copy for $name<'_, '_, T, L, B> {}
155            impl<T, L, B> Clone for $name<'_, '_, T, L, B> {
156                fn clone(&self) -> Self {
157                    *self
158                }
159            }
160
161            impl<'a, 'slf, T: 'a, L, B> FreeVariableWithContextWithProps<OperatorContext<L, B>, ()>
162                for $name<'a, 'slf, T, L, B>
163            where
164                L: Location<'a>,
165            {
166                type O = $output;
167
168                fn to_tokens(self, _ctx: &OperatorContext<L, B>) -> (QuoteTokens, ()) {
169                    let ident = register_handoff_ref(
170                        self.ir_node,
171                        $is_mut,
172                        $kind,
173                    );
174                    (
175                        QuoteTokens {
176                            prelude: None,
177                            expr: Some(quote!(#ident)),
178                        },
179                        (),
180                    )
181                }
182            }
183        )+
184    };
185}
186
187#[stageleft::export(
188    SingletonRef,
189    SingletonMut,
190    OptionalRef,
191    OptionalMut,
192    StreamRef,
193    StreamMut
194)]
195define_handoff_ref!(
196    /// A shared reference handle to a singleton, resolves to `&T` at runtime.
197    ///
198    /// Created via [`Singleton::by_ref()`](crate::live_collections::Singleton::by_ref).
199    SingletonRef, false, HandoffRefKind::Singleton, &'a T
200
201    /// A mutable reference handle to a singleton, resolves to `&mut T` at runtime.
202    ///
203    /// Created via [`Singleton::by_mut()`](crate::live_collections::Singleton::by_mut).
204    SingletonMut, true, HandoffRefKind::Singleton, &'a mut T
205
206    /// A shared reference handle to an optional, resolves to `&Option<T>` at runtime.
207    ///
208    /// Created via [`Optional::by_ref()`](crate::live_collections::Optional::by_ref).
209    OptionalRef, false, HandoffRefKind::Optional, &'a Option<T>
210
211    /// A mutable reference handle to an optional, resolves to `&mut Option<T>` at runtime.
212    ///
213    /// Created via [`Optional::by_mut()`](crate::live_collections::Optional::by_mut).
214    OptionalMut, true, HandoffRefKind::Optional, &'a mut Option<T>
215
216    /// A shared reference handle to a stream's handoff buffer, resolves to `&Vec<T>` at runtime.
217    ///
218    /// Created via [`Stream::by_ref()`](crate::live_collections::Stream::by_ref).
219    StreamRef, false, HandoffRefKind::Vec, &'a Vec<T>
220
221    /// A mutable reference handle to a stream's handoff buffer, resolves to `&mut Vec<T>` at runtime.
222    ///
223    /// Created via [`Stream::by_mut()`](crate::live_collections::Stream::by_mut).
224    StreamMut, true, HandoffRefKind::Vec, &'a mut Vec<T>
225);
226
227#[cfg(test)]
228#[cfg(feature = "build")]
229mod tests {
230    use stageleft::q;
231
232    use crate::compile::builder::FlowBuilder;
233    use crate::location::Location;
234
235    struct P1 {}
236
237    /// Compile-only test: verifies that `by_ref()` + `q!()` produces valid IR.
238    #[test]
239    fn singleton_by_ref_compiles() {
240        let mut flow = FlowBuilder::new();
241        let node = flow.process::<P1>();
242
243        let my_count = node
244            .source_iter(q!(0..5i32))
245            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
246        let count_ref = my_count.by_ref();
247
248        node.source_iter(q!(1..=3i32))
249            .map(q!(|x| x + *count_ref))
250            .for_each(q!(|_| {}));
251
252        my_count.into_stream().for_each(q!(|_| {}));
253        let _built = flow.finalize();
254    }
255
256    /// Test with a non-Copy type (Vec) to ensure we're borrowing, not copying.
257    #[test]
258    fn singleton_by_ref_non_copy() {
259        let mut flow = FlowBuilder::new();
260        let node = flow.process::<P1>();
261
262        let my_vec = node.source_iter(q!(0..5i32)).fold(
263            q!(|| Vec::<i32>::new()),
264            q!(|acc: &mut Vec<i32>, x| acc.push(x)),
265        );
266        let vec_ref = my_vec.by_ref();
267
268        node.source_iter(q!(1..=3i32))
269            .map(q!(|x| x + vec_ref.len() as i32))
270            .for_each(q!(|_| {}));
271
272        my_vec.into_stream().for_each(q!(|_| {}));
273        let _built = flow.finalize();
274    }
275
276    /// Compile-only: singleton ref inside filter closure.
277    #[test]
278    fn singleton_by_ref_filter() {
279        let mut flow = FlowBuilder::new();
280        let node = flow.process::<P1>();
281
282        let threshold = node
283            .source_iter(q!(0..5i32))
284            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
285        let threshold_ref = threshold.by_ref();
286
287        node.source_iter(q!(1..=10i32))
288            .filter(q!(|x| *x > *threshold_ref))
289            .for_each(q!(|_| {}));
290
291        threshold.into_stream().for_each(q!(|_| {}));
292        let _built = flow.finalize();
293    }
294
295    /// Compile-only: singleton ref inside flat_map closure.
296    #[test]
297    fn singleton_by_ref_flat_map() {
298        let mut flow = FlowBuilder::new();
299        let node = flow.process::<P1>();
300
301        let count = node
302            .source_iter(q!(0..3i32))
303            .fold(q!(|| 0i32), q!(|acc: &mut i32, _| *acc += 1));
304        let count_ref = count.by_ref();
305
306        node.source_iter(q!(1..=2i32))
307            .flat_map_ordered(q!(|x| (0..*count_ref).map(move |i| x + i)))
308            .for_each(q!(|_| {}));
309
310        count.into_stream().for_each(q!(|_| {}));
311        let _built = flow.finalize();
312    }
313
314    /// Compile-only: singleton ref inside inspect closure.
315    #[test]
316    fn singleton_by_ref_inspect() {
317        let mut flow = FlowBuilder::new();
318        let node = flow.process::<P1>();
319
320        let count = node
321            .source_iter(q!(0..5i32))
322            .fold(q!(|| 0i32), q!(|acc: &mut i32, _| *acc += 1));
323        let count_ref = count.by_ref();
324
325        node.source_iter(q!(1..=3i32))
326            .inspect(q!(|x| println!("count={}, x={}", *count_ref, x)))
327            .for_each(q!(|_| {}));
328
329        count.into_stream().for_each(q!(|_| {}));
330        let _built = flow.finalize();
331    }
332
333    /// Compile-only: singleton ref inside partition predicate.
334    #[test]
335    fn singleton_by_ref_partition() {
336        let mut flow = FlowBuilder::new();
337        let node = flow.process::<P1>();
338
339        let threshold = node
340            .source_iter(q!(0..5i32))
341            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
342        let threshold_ref = threshold.by_ref();
343
344        let (above, below) = node
345            .source_iter(q!(1..=10i32))
346            .partition(q!(|x| *x > *threshold_ref));
347
348        above.for_each(q!(|_| {}));
349        below.for_each(q!(|_| {}));
350        threshold.into_stream().for_each(q!(|_| {}));
351        let _built = flow.finalize();
352    }
353
354    /// Compile-only: singleton ref inside partition with downstream operators on both branches.
355    #[test]
356    fn singleton_by_ref_partition_with_downstream_ops() {
357        let mut flow = FlowBuilder::new();
358        let node = flow.process::<P1>();
359
360        let threshold = node
361            .source_iter(q!(0..5i32))
362            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
363        let threshold_ref = threshold.by_ref();
364
365        let (above, below) = node
366            .source_iter(q!(1..=10i32))
367            .partition(q!(|x| *x > *threshold_ref));
368
369        above.map(q!(|x| x * 2)).for_each(q!(|_| {}));
370        below.map(q!(|x| x + 100)).for_each(q!(|_| {}));
371        threshold.into_stream().for_each(q!(|_| {}));
372        let _built = flow.finalize();
373    }
374
375    /// Compile-only test: singleton by_mut.
376    #[test]
377    fn singleton_by_mut_compiles() {
378        let mut flow = FlowBuilder::new();
379        let node = flow.process::<P1>();
380
381        let my_count = node
382            .source_iter(q!(0..5i32))
383            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
384        let count_mut = my_count.by_mut();
385
386        node.source_iter(q!(1..=3i32))
387            .map(q!(|x| {
388                *count_mut += x;
389                x
390            }))
391            .for_each(q!(|_| {}));
392
393        my_count.into_stream().for_each(q!(|_| {}));
394        let _built = flow.finalize();
395    }
396
397    /// Compile-only test: optional by_ref.
398    #[test]
399    fn optional_by_ref_compiles() {
400        let mut flow = FlowBuilder::new();
401        let node = flow.process::<P1>();
402
403        let my_opt = node.source_iter(q!(0..5i32)).reduce(q!(|a, b| *a += b));
404        let opt_ref = my_opt.by_ref();
405
406        node.source_iter(q!(1..=3i32))
407            .map(q!(|x| x + opt_ref.unwrap_or(0)))
408            .for_each(q!(|_| {}));
409
410        my_opt.into_stream().for_each(q!(|_| {}));
411        let _built = flow.finalize();
412    }
413
414    /// Compile-only test: stream by_ref.
415    #[test]
416    fn stream_by_ref_compiles() {
417        let mut flow = FlowBuilder::new();
418        let node = flow.process::<P1>();
419
420        let my_stream = node.source_iter(q!(0..5i32));
421        let stream_ref = my_stream.by_ref();
422
423        node.source_iter(q!(1..=3i32))
424            .map(q!(|x| x + stream_ref.len() as i32))
425            .for_each(q!(|_| {}));
426
427        my_stream.for_each(q!(|_| {}));
428        let _built = flow.finalize();
429    }
430
431    /// Compile-only test: singleton by_mut in filter (TotalOrder).
432    #[test]
433    fn singleton_by_mut_filter() {
434        let mut flow = FlowBuilder::new();
435        let node = flow.process::<P1>();
436
437        let my_count = node
438            .source_iter(q!(0..5i32))
439            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
440        let count_mut = my_count.by_mut();
441
442        node.source_iter(q!(1..=3i32))
443            .filter(q!(|x| {
444                *count_mut += *x;
445                *count_mut > 0
446            }))
447            .for_each(q!(|_| {}));
448
449        my_count.into_stream().for_each(q!(|_| {}));
450        let _built = flow.finalize();
451    }
452
453    /// Compile-only test: singleton by_mut in flat_map_ordered (TotalOrder).
454    #[test]
455    fn singleton_by_mut_flat_map() {
456        let mut flow = FlowBuilder::new();
457        let node = flow.process::<P1>();
458
459        let my_count = node
460            .source_iter(q!(0..5i32))
461            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
462        let count_mut = my_count.by_mut();
463
464        node.source_iter(q!(1..=3i32))
465            .flat_map_ordered(q!(|x| {
466                *count_mut += x;
467                vec![*count_mut]
468            }))
469            .for_each(q!(|_| {}));
470
471        my_count.into_stream().for_each(q!(|_| {}));
472        let _built = flow.finalize();
473    }
474
475    /// Compile-only test: singleton by_mut in filter_map (TotalOrder).
476    #[test]
477    fn singleton_by_mut_filter_map() {
478        let mut flow = FlowBuilder::new();
479        let node = flow.process::<P1>();
480
481        let my_count = node
482            .source_iter(q!(0..5i32))
483            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
484        let count_mut = my_count.by_mut();
485
486        node.source_iter(q!(1..=3i32))
487            .filter_map(q!(|x| {
488                *count_mut += x;
489                Some(*count_mut)
490            }))
491            .for_each(q!(|_| {}));
492
493        my_count.into_stream().for_each(q!(|_| {}));
494        let _built = flow.finalize();
495    }
496
497    /// Compile-only test: singleton by_mut in inspect (TotalOrder).
498    #[test]
499    fn singleton_by_mut_inspect() {
500        let mut flow = FlowBuilder::new();
501        let node = flow.process::<P1>();
502
503        let my_count = node
504            .source_iter(q!(0..5i32))
505            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
506        let count_mut = my_count.by_mut();
507
508        node.source_iter(q!(1..=3i32))
509            .inspect(q!(|x| {
510                *count_mut += *x;
511            }))
512            .for_each(q!(|_| {}));
513
514        my_count.into_stream().for_each(q!(|_| {}));
515        let _built = flow.finalize();
516    }
517
518    /// Compile-only test: singleton by_ref in for_each.
519    #[test]
520    fn singleton_by_ref_for_each() {
521        let mut flow = FlowBuilder::new();
522        let node = flow.process::<P1>();
523
524        let my_count = node
525            .source_iter(q!(0..5i32))
526            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
527        let count_ref = my_count.by_ref();
528
529        node.source_iter(q!(1..=3i32))
530            .for_each(q!(|x| println!("{}", x + *count_ref)));
531
532        my_count.into_stream().for_each(q!(|_| {}));
533        let _built = flow.finalize();
534    }
535
536    /// Compile-only test: singleton by_mut in for_each.
537    #[test]
538    fn singleton_by_mut_for_each() {
539        let mut flow = FlowBuilder::new();
540        let node = flow.process::<P1>();
541
542        let my_count = node
543            .source_iter(q!(0..5i32))
544            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
545        let count_mut = my_count.by_mut();
546
547        node.source_iter(q!(1..=3i32)).for_each(q!(|x| {
548            *count_mut += x;
549        }));
550
551        my_count.into_stream().for_each(q!(|_| {}));
552        let _built = flow.finalize();
553    }
554
555    /// Regression test: a handoff reference whose *only* consumer is a `for_each` closure
556    /// must still be materialized during DFIR emission.
557    ///
558    /// `HydroRoot::ForEach` used to only *look up* captured refs in `built_tees`, assuming
559    /// some node-level operator had already emitted them, and panicked with "ForEach
560    /// singleton ref not found in built_tees" when the `for_each` closure was the sole
561    /// capturer. This test drives the flow through full DFIR emission (which
562    /// `flow.finalize()` alone does not) to cover that path.
563    #[cfg(feature = "deploy")]
564    #[test]
565    fn singleton_by_ref_for_each_sole_consumer_emits() {
566        use crate::live_collections::sliced::sliced;
567        use crate::nondet::nondet;
568
569        let mut flow = FlowBuilder::new();
570        let node = flow.process::<P1>();
571
572        let items = node.source_iter(q!(1..=3i32));
573
574        sliced! {
575            let items = use::batch(items, nondet!(/** test */));
576            let my_count = items
577                .location()
578                .source_iter(q!(0..5i32))
579                .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
580            let count_ref = my_count.by_ref();
581
582            // The for_each closure is the ONLY consumer of `my_count` — no other operator
583            // emits the reference node before this root is processed.
584            items.for_each(q!(|x| println!("{}", x + *count_ref)));
585        };
586
587        let _ = flow
588            .finalize()
589            .with_default_optimize::<crate::deploy::HydroDeploy>()
590            .preview_compile();
591    }
592
593    /// Regression test: same as [`singleton_by_ref_for_each_sole_consumer_emits`], but for
594    /// a mutable reference (`by_mut`) — the common accumulator pattern
595    /// `stream.for_each(q!(|x| *acc_mut += x))`.
596    #[cfg(feature = "deploy")]
597    #[test]
598    fn singleton_by_mut_for_each_sole_consumer_emits() {
599        use crate::live_collections::sliced::sliced;
600        use crate::nondet::nondet;
601
602        let mut flow = FlowBuilder::new();
603        let node = flow.process::<P1>();
604
605        let items = node.source_iter(q!(1..=3i32));
606
607        sliced! {
608            let items = use::batch(items, nondet!(/** test */));
609            let my_count = items
610                .location()
611                .source_iter(q!(0..5i32))
612                .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
613            let count_mut = my_count.by_mut();
614
615            // The for_each closure is the ONLY consumer of `my_count`.
616            items.for_each(q!(|x| {
617                *count_mut += x;
618            }));
619        };
620
621        let _ = flow
622            .finalize()
623            .with_default_optimize::<crate::deploy::HydroDeploy>()
624            .preview_compile();
625    }
626
627    /// Regression test: a singleton ref captured by a partition predicate must be
628    /// spliced correctly during DFIR emission. The `PartitionShared` emit previously
629    /// drained the ident stack in the wrong order (calling `emit_tokens` before popping
630    /// the input ident), so the partition's *input* ident was consumed as if it were the
631    /// closure's singleton ref. This test drives the flow through full DFIR emission
632    /// (which `flow.finalize()` alone does not) to cover that path.
633    #[cfg(feature = "deploy")]
634    #[test]
635    fn singleton_by_ref_partition_emits() {
636        let mut flow = FlowBuilder::new();
637        let node = flow.process::<P1>();
638
639        let threshold = node
640            .source_iter(q!(0..5i32))
641            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
642        let threshold_ref = threshold.by_ref();
643
644        let (above, below) = node
645            .source_iter(q!(1..=10i32))
646            .partition(q!(|x| *x > *threshold_ref));
647
648        above.for_each(q!(|_| {}));
649        below.for_each(q!(|_| {}));
650        threshold.into_stream().for_each(q!(|_| {}));
651
652        let _ = flow
653            .finalize()
654            .with_default_optimize::<crate::deploy::HydroDeploy>()
655            .preview_compile();
656    }
657
658    /// Compile-only test: singleton by_ref inside scan closures.
659    #[test]
660    fn singleton_by_ref_scan() {
661        let mut flow = FlowBuilder::new();
662        let node = flow.process::<P1>();
663
664        let offset = node
665            .source_iter(q!(0..5i32))
666            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
667        let offset_ref = offset.by_ref();
668
669        node.source_iter(q!(1..=3i32))
670            .scan(
671                q!(move || *offset_ref),
672                q!(move |acc: &mut i32, x| {
673                    *acc += x + *offset_ref;
674                    Some(*acc)
675                }),
676            )
677            .for_each(q!(|_| {}));
678
679        offset.into_stream().for_each(q!(|_| {}));
680        let _built = flow.finalize();
681    }
682
683    /// Compile-only test: singleton by_ref inside scan_async_blocking closure.
684    #[test]
685    fn singleton_by_ref_scan_async_blocking() {
686        let mut flow = FlowBuilder::new();
687        let node = flow.process::<P1>();
688
689        let offset = node
690            .source_iter(q!(0..5i32))
691            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
692        let offset_ref = offset.by_ref();
693
694        node.source_iter(q!(1..=3i32))
695            .scan_async_blocking(
696                q!(|| 0i32),
697                q!(move |acc: &mut i32, x| {
698                    *acc += x + *offset_ref;
699                    let val = *acc;
700                    async move { Some(val) }
701                }),
702            )
703            .for_each(q!(|_| {}));
704
705        offset.into_stream().for_each(q!(|_| {}));
706        let _built = flow.finalize();
707    }
708
709    /// Compile-only test: singleton by_ref inside generator closure.
710    #[test]
711    fn singleton_by_ref_generator() {
712        use crate::live_collections::keyed_stream::Generate;
713
714        let mut flow = FlowBuilder::new();
715        let node = flow.process::<P1>();
716
717        let threshold = node
718            .source_iter(q!(0..5i32))
719            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
720        let threshold_ref = threshold.by_ref();
721
722        node.source_iter(q!(1..=3i32))
723            .generator(
724                q!(|| 0i32),
725                q!(move |acc: &mut i32, x| {
726                    *acc += x;
727                    if *acc > *threshold_ref {
728                        Generate::Return(*acc)
729                    } else {
730                        Generate::Yield(*acc)
731                    }
732                }),
733            )
734            .for_each(q!(|_| {}));
735
736        threshold.into_stream().for_each(q!(|_| {}));
737        let _built = flow.finalize();
738    }
739
740    /// Compile-only test: singleton by_ref inside keyed scan closure.
741    #[test]
742    fn singleton_by_ref_keyed_scan() {
743        let mut flow = FlowBuilder::new();
744        let node = flow.process::<P1>();
745
746        let offset = node
747            .source_iter(q!(0..5i32))
748            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
749        let offset_ref = offset.by_ref();
750
751        node.source_iter(q!(vec![(0, 1i32), (1, 2i32)]))
752            .into_keyed()
753            .scan(
754                q!(|| 0i32),
755                q!(move |acc: &mut i32, x| {
756                    *acc += x + *offset_ref;
757                    Some(*acc)
758                }),
759            )
760            .entries()
761            .assume_ordering::<crate::live_collections::stream::TotalOrder>(
762                crate::nondet::nondet!(/** test */),
763            )
764            .for_each(q!(|_| {}));
765
766        offset.into_stream().for_each(q!(|_| {}));
767        let _built = flow.finalize();
768    }
769
770    /// Compile-only test: singleton by_ref inside keyed generator closure.
771    #[test]
772    fn singleton_by_ref_keyed_generator() {
773        use crate::live_collections::keyed_stream::Generate;
774
775        let mut flow = FlowBuilder::new();
776        let node = flow.process::<P1>();
777
778        let threshold = node
779            .source_iter(q!(0..5i32))
780            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
781        let threshold_ref = threshold.by_ref();
782
783        node.source_iter(q!(vec![(0, 1i32), (1, 2i32)]))
784            .into_keyed()
785            .generator(
786                q!(|| 0i32),
787                q!(move |acc: &mut i32, x| {
788                    *acc += x;
789                    if *acc > *threshold_ref {
790                        Generate::Return(*acc)
791                    } else {
792                        Generate::Yield(*acc)
793                    }
794                }),
795            )
796            .entries()
797            .assume_ordering::<crate::live_collections::stream::TotalOrder>(
798                crate::nondet::nondet!(/** test */),
799            )
800            .for_each(q!(|_| {}));
801
802        threshold.into_stream().for_each(q!(|_| {}));
803        let _built = flow.finalize();
804    }
805
806    /// Regression test for the `Partition` dedup path in `HydroNode::transform_children`.
807    ///
808    /// A `partition` whose predicate closure captures a `by_ref` singleton produces two output
809    /// branches that share the partition's `inner` but each own their own clone of the closure.
810    /// When the shared `inner` is rewritten (during `finalize` → `unify_atomic_ticks` →
811    /// `transform_bottom_up`), the first branch's closure is transformed and the referenced cell
812    /// is emptied to `HydroNode::Placeholder`. The *deduplicated* second branch used to be skipped
813    /// entirely, leaving its captured `singleton_ref` dangling at that `Placeholder` cell. This
814    /// asserts that no closure `singleton_ref` resolves to a `Placeholder` after finalization.
815    #[test]
816    fn partition_dedup_does_not_leave_placeholder_refs() {
817        use std::cell::RefCell;
818        use std::collections::HashSet;
819
820        use crate::compile::ir::{ClosureExpr, HydroNode};
821
822        fn closures(node: &HydroNode) -> Vec<&ClosureExpr> {
823            match node {
824                HydroNode::Map { f, .. }
825                | HydroNode::FlatMap { f, .. }
826                | HydroNode::FlatMapStreamBlocking { f, .. }
827                | HydroNode::Filter { f, .. }
828                | HydroNode::FilterMap { f, .. }
829                | HydroNode::Inspect { f, .. }
830                | HydroNode::Reduce { f, .. }
831                | HydroNode::ReduceKeyed { f, .. }
832                | HydroNode::ReduceKeyedWatermark { f, .. }
833                | HydroNode::PartitionShared { f, .. } => vec![f],
834                HydroNode::Fold { init, acc, .. }
835                | HydroNode::FoldKeyed { init, acc, .. }
836                | HydroNode::Scan { init, acc, .. }
837                | HydroNode::ScanAsyncBlocking { init, acc, .. } => vec![init, acc],
838                _ => vec![],
839            }
840        }
841
842        fn count_placeholder_refs(
843            node: &HydroNode,
844            visited: &mut HashSet<*const RefCell<HydroNode>>,
845            count: &mut usize,
846        ) {
847            // A captured `singleton_ref` should never resolve to a `Placeholder`.
848            for f in closures(node) {
849                for (ref_node, _is_mut) in &f.singleton_refs {
850                    if let HydroNode::Reference { inner, .. } = ref_node
851                        && matches!(&*inner.0.borrow(), HydroNode::Placeholder)
852                    {
853                        *count += 1;
854                    }
855                }
856            }
857
858            // Recurse the (owned) tree children.
859            for child in node.input() {
860                count_placeholder_refs(child, visited, count);
861            }
862
863            // Recurse the shared inner once, guarding against `Placeholder` / revisits.
864            let shared_inner = match node {
865                HydroNode::Tee { inner, .. }
866                | HydroNode::Reference { inner, .. }
867                | HydroNode::PartitionSide { inner, .. } => Some(inner),
868                _ => None,
869            };
870            if let Some(inner) = shared_inner
871                && visited.insert(inner.as_ptr())
872            {
873                let borrowed = inner.0.borrow();
874                if !matches!(&*borrowed, HydroNode::Placeholder) {
875                    count_placeholder_refs(&borrowed, visited, count);
876                }
877            }
878        }
879
880        let mut flow = FlowBuilder::new();
881        let node = flow.process::<P1>();
882
883        let threshold = node
884            .source_iter(q!(0..5i32))
885            .fold(q!(|| 0i32), q!(|acc: &mut i32, x| *acc += x));
886        let threshold_ref = threshold.by_ref();
887
888        let (above, below) = node
889            .source_iter(q!(vec![5i32, 8, 10, 11, 15, 3]))
890            .partition(q!(|x| *x > *threshold_ref));
891
892        above.for_each(q!(|_| {}));
893        below.for_each(q!(|_| {}));
894        threshold.into_stream().for_each(q!(|_| {}));
895
896        let built = flow.finalize();
897
898        let mut visited = HashSet::new();
899        let mut count = 0usize;
900        for root in built.ir() {
901            count_placeholder_refs(root.input(), &mut visited, &mut count);
902        }
903
904        assert_eq!(
905            count, 0,
906            "partition's deduplicated branch left {count} closure singleton_ref(s) dangling at a Placeholder cell"
907        );
908    }
909}