Skip to main content

hydro_lang/compile/ir/
mod.rs

1use core::panic;
2use std::cell::{Cell, RefCell};
3use std::collections::HashMap;
4#[cfg(feature = "build")]
5use std::collections::HashSet;
6use std::fmt::{Debug, Display};
7use std::hash::{Hash, Hasher};
8use std::ops::Deref;
9use std::rc::Rc;
10
11#[cfg(feature = "build")]
12use dfir_lang::graph::FlatGraphBuilder;
13#[cfg(feature = "build")]
14use proc_macro2::Span;
15use proc_macro2::TokenStream;
16use quote::ToTokens;
17#[cfg(feature = "build")]
18use quote::quote;
19#[cfg(feature = "build")]
20use slotmap::{SecondaryMap, SparseSecondaryMap};
21#[cfg(feature = "build")]
22use syn::parse_quote;
23
24#[cfg(feature = "build")]
25use crate::compile::builder::ClockId;
26#[cfg(feature = "build")]
27use crate::compile::builder::StmtId;
28use crate::compile::builder::{CycleId, ExternalPortId};
29#[cfg(feature = "build")]
30use crate::compile::deploy_provider::{Deploy, Node, RegisterPort};
31#[cfg(feature = "build")]
32use crate::handoff_ref::handoff_ref_ident;
33use crate::location::dynamic::{ClusterConsistency, LocationId};
34use crate::location::{LocationKey, NetworkHint};
35
36pub mod backtrace;
37use backtrace::Backtrace;
38
39/// A closure expression bundled with any singleton references it captures.
40///
41/// When a `q!()` closure captures a `SingletonRef`, the reference is recorded here
42/// alongside the closure's expression. This allows per-closure tracking of singleton
43/// captures, which is important for nodes with multiple closures (e.g. Fold has `init` and `acc`).
44pub struct ClosureExpr {
45    pub(crate) expr: DebugExpr,
46    /// Each entry is `(HydroNode::Reference, is_mut: bool)`.
47    /// The index in the Vec determines the ident name via [`handoff_ref_ident`].
48    /// The `access_counter` was assigned at staging time in code order.
49    pub(crate) singleton_refs: Vec<(HydroNode, bool)>,
50}
51
52impl Clone for ClosureExpr {
53    fn clone(&self) -> Self {
54        Self {
55            expr: self.expr.clone(),
56            singleton_refs: self
57                .singleton_refs
58                .iter()
59                .map(|(node, is_mut)| {
60                    let HydroNode::Reference {
61                        inner,
62                        kind,
63                        access_counter,
64                        metadata,
65                    } = node
66                    else {
67                        panic!("singleton_refs should only contain HydroNode::Reference");
68                    };
69                    (
70                        HydroNode::Reference {
71                            inner: SharedNode(Rc::clone(&inner.0)),
72                            kind: *kind,
73                            access_counter: access_counter.freeze(),
74                            metadata: metadata.clone(),
75                        },
76                        *is_mut,
77                    )
78                })
79                .collect(),
80        }
81    }
82}
83
84impl Hash for ClosureExpr {
85    fn hash<H: Hasher>(&self, state: &mut H) {
86        self.expr.hash(state);
87        // singleton_refs are structural children (like HydroIrMetadata), not
88        // identity-defining. Two closures with the same expr but different
89        // captured refs are the same closure text — the refs only affect codegen.
90    }
91}
92
93impl serde::Serialize for ClosureExpr {
94    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
95        use serde::ser::SerializeStruct;
96        let mut s = serializer.serialize_struct("ClosureExpr", 2)?;
97        s.serialize_field("expr", &self.expr)?;
98        s.serialize_field(
99            "singleton_refs",
100            &SerializableSingletonRefs(&self.singleton_refs),
101        )?;
102        s.end()
103    }
104}
105
106struct SerializableSingletonRefs<'a>(&'a [(HydroNode, bool)]);
107
108impl serde::Serialize for SerializableSingletonRefs<'_> {
109    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
110        use serde::ser::SerializeSeq;
111        let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
112        for (node, is_mut) in self.0.iter() {
113            seq.serialize_element(&(node, is_mut))?;
114        }
115        seq.end()
116    }
117}
118
119impl Debug for ClosureExpr {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        Debug::fmt(&self.expr, f)
122    }
123}
124
125impl Display for ClosureExpr {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        Display::fmt(&self.expr, f)
128    }
129}
130
131impl From<syn::Expr> for ClosureExpr {
132    fn from(expr: syn::Expr) -> Self {
133        Self {
134            expr: DebugExpr(Box::new(expr)),
135            singleton_refs: Vec::new(),
136        }
137    }
138}
139
140impl From<DebugExpr> for ClosureExpr {
141    fn from(expr: DebugExpr) -> Self {
142        Self {
143            expr,
144            singleton_refs: Vec::new(),
145        }
146    }
147}
148
149impl ClosureExpr {
150    pub fn new(expr: DebugExpr, singleton_refs: Vec<(HydroNode, bool)>) -> Self {
151        Self {
152            expr,
153            singleton_refs,
154        }
155    }
156
157    pub fn has_mut_ref(&self) -> bool {
158        self.singleton_refs.iter().any(|(_, is_mut)| *is_mut)
159    }
160
161    pub fn deep_clone(&self, seen_tees: &mut SeenSharedNodes) -> Self {
162        Self {
163            expr: self.expr.clone(),
164            singleton_refs: self
165                .singleton_refs
166                .iter()
167                .map(|(node, is_mut)| (node.deep_clone(seen_tees), *is_mut))
168                .collect(),
169        }
170    }
171
172    pub fn transform_children(
173        &mut self,
174        transform: &mut impl FnMut(&mut HydroNode, &mut SeenSharedNodes),
175        seen_tees: &mut SeenSharedNodes,
176    ) {
177        for (ref_node, _is_mut) in self.singleton_refs.iter_mut() {
178            transform(ref_node, seen_tees);
179        }
180    }
181
182    /// Pop singleton ref idents from the stack and rewrite the closure's token stream,
183    /// replacing local singleton ref idents with `#{N} dfir_ident` or `#{N} mut dfir_ident` references.
184    #[cfg(feature = "build")]
185    pub fn emit_tokens(&self, ident_stack: &mut Vec<syn::Ident>) -> TokenStream {
186        if self.singleton_refs.is_empty() {
187            self.expr.0.to_token_stream()
188        } else {
189            assert!(
190                ident_stack.len() >= self.singleton_refs.len(),
191                "ident_stack has {} entries but expected at least {} for singleton_refs",
192                ident_stack.len(),
193                self.singleton_refs.len()
194            );
195            let ref_idents = ident_stack.drain(ident_stack.len() - self.singleton_refs.len()..);
196
197            let mut let_bindings = Vec::new();
198            for ((i, (ref_node, is_mut)), ref_ident) in
199                self.singleton_refs.iter().enumerate().zip(ref_idents)
200            {
201                let HydroNode::Reference { access_counter, .. } = ref_node else {
202                    panic!("ClosureExpression expected references to `HydroNode::Reference`");
203                };
204                let group = access_counter.frozen_group();
205                // TODO(mingwei): proper spanning?
206                let local_ident = handoff_ref_ident(i);
207                let hash = proc_macro2::Punct::new('#', proc_macro2::Spacing::Alone);
208                let group_lit = proc_macro2::Literal::u32_unsuffixed(group);
209                let mut_token = is_mut.then(|| quote!(mut));
210                let binding = quote! {
211                    let #local_ident = #hash {#group_lit} #mut_token #ref_ident;
212                };
213                let_bindings.push(binding);
214            }
215
216            let expr = &self.expr.0;
217            quote! {
218                {
219                    #( #let_bindings )*
220                    #expr
221                }
222            }
223        }
224    }
225}
226
227/// Wrapper that displays only the tokens of a parsed expr.
228///
229/// Boxes `syn::Type` which is ~240 bytes.
230#[derive(Clone, Hash)]
231pub struct DebugExpr(pub Box<syn::Expr>);
232
233impl serde::Serialize for DebugExpr {
234    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
235        serializer.serialize_str(&self.to_string())
236    }
237}
238
239impl From<syn::Expr> for DebugExpr {
240    fn from(expr: syn::Expr) -> Self {
241        Self(Box::new(expr))
242    }
243}
244
245impl Deref for DebugExpr {
246    type Target = syn::Expr;
247
248    fn deref(&self) -> &Self::Target {
249        &self.0
250    }
251}
252
253impl ToTokens for DebugExpr {
254    fn to_tokens(&self, tokens: &mut TokenStream) {
255        self.0.to_tokens(tokens);
256    }
257}
258
259impl Debug for DebugExpr {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        write!(f, "{}", self.0.to_token_stream())
262    }
263}
264
265impl Display for DebugExpr {
266    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        let original = self.0.as_ref().clone();
268        let simplified = simplify_q_macro(original);
269
270        // For now, just use quote formatting without trying to parse as a statement
271        // This avoids the syn::parse_quote! issues entirely
272        write!(f, "q!({})", quote::quote!(#simplified))
273    }
274}
275
276/// Simplify expanded q! macro calls back to q!(...) syntax for better readability
277fn simplify_q_macro(expr: syn::Expr) -> syn::Expr {
278    if let syn::Expr::Call(ref call) = expr && let syn::Expr::Path(path_expr) = call.func.as_ref()
279        // Look for calls to stageleft::runtime_support::fn*
280        && is_stageleft_runtime_support_call(&path_expr.path)
281        && let syn::Expr::Block(b) = &call.args[0]
282        && b.block.stmts.len() == 3
283        && let Some(syn::Stmt::Expr(e, _)) = b.block.stmts.get(2)
284    // skip the first two, which are imports
285    {
286        let mut e = e.clone();
287        while let syn::Expr::Block(ref mut block) = e
288            && block.block.stmts.len() == 1
289            && let syn::Stmt::Expr(inner_e, _) = block.block.stmts.remove(0)
290        {
291            e = inner_e;
292        }
293
294        e
295    } else {
296        expr
297    }
298}
299
300fn is_stageleft_runtime_support_call(path: &syn::Path) -> bool {
301    // Check if this is a call to stageleft::runtime_support::fn*
302    if let Some(last_segment) = path.segments.last() {
303        let fn_name = last_segment.ident.to_string();
304        path.segments.len() > 2
305            && path.segments[0].ident == "stageleft"
306            && path.segments[1].ident == "runtime_support"
307            && fn_name.contains("_type_hint")
308    } else {
309        false
310    }
311}
312
313/// Debug displays the type's tokens.
314///
315/// Boxes `syn::Type` which is ~320 bytes.
316#[derive(Clone, PartialEq, Eq, Hash)]
317pub struct DebugType(pub Box<syn::Type>);
318
319impl From<syn::Type> for DebugType {
320    fn from(t: syn::Type) -> Self {
321        Self(Box::new(t))
322    }
323}
324
325impl Deref for DebugType {
326    type Target = syn::Type;
327
328    fn deref(&self) -> &Self::Target {
329        &self.0
330    }
331}
332
333impl ToTokens for DebugType {
334    fn to_tokens(&self, tokens: &mut TokenStream) {
335        self.0.to_tokens(tokens);
336    }
337}
338
339impl Debug for DebugType {
340    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341        write!(f, "{}", self.0.to_token_stream())
342    }
343}
344
345impl serde::Serialize for DebugType {
346    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
347        serializer.serialize_str(&format!("{}", self.0.to_token_stream()))
348    }
349}
350
351fn serialize_backtrace_as_span<S: serde::Serializer>(
352    backtrace: &Backtrace,
353    serializer: S,
354) -> Result<S::Ok, S::Error> {
355    match backtrace.format_span() {
356        Some(span) => serializer.serialize_some(&span),
357        None => serializer.serialize_none(),
358    }
359}
360
361fn serialize_ident<S: serde::Serializer>(
362    ident: &syn::Ident,
363    serializer: S,
364) -> Result<S::Ok, S::Error> {
365    serializer.serialize_str(&ident.to_string())
366}
367
368pub enum DebugInstantiate {
369    Building,
370    Finalized(Box<DebugInstantiateFinalized>),
371}
372
373impl serde::Serialize for DebugInstantiate {
374    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
375        match self {
376            DebugInstantiate::Building => {
377                serializer.serialize_unit_variant("DebugInstantiate", 0, "Building")
378            }
379            DebugInstantiate::Finalized(_) => {
380                panic!(
381                    "cannot serialize DebugInstantiate::Finalized: contains non-serializable runtime state (closures)"
382                )
383            }
384        }
385    }
386}
387
388#[cfg_attr(
389    not(feature = "build"),
390    expect(
391        dead_code,
392        reason = "sink, source unused without `feature = \"build\"`."
393    )
394)]
395pub struct DebugInstantiateFinalized {
396    sink: syn::Expr,
397    source: syn::Expr,
398    connect_fn: Option<Box<dyn FnOnce()>>,
399}
400
401impl From<DebugInstantiateFinalized> for DebugInstantiate {
402    fn from(f: DebugInstantiateFinalized) -> Self {
403        Self::Finalized(Box::new(f))
404    }
405}
406
407impl Debug for DebugInstantiate {
408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409        write!(f, "<network instantiate>")
410    }
411}
412
413impl Hash for DebugInstantiate {
414    fn hash<H: Hasher>(&self, _state: &mut H) {
415        // Do nothing
416    }
417}
418
419impl Clone for DebugInstantiate {
420    fn clone(&self) -> Self {
421        match self {
422            DebugInstantiate::Building => DebugInstantiate::Building,
423            DebugInstantiate::Finalized(_) => {
424                panic!("DebugInstantiate::Finalized should not be cloned")
425            }
426        }
427    }
428}
429
430/// Tracks the instantiation state of a `ClusterMembers` source.
431///
432/// During `compile_network`, the first `ClusterMembers` node for a given
433/// `(at_location, target_cluster)` pair is promoted to [`Self::Stream`] and
434/// receives the expression returned by `Deploy::cluster_membership_stream`.
435/// All subsequent nodes for the same pair are set to [`Self::Tee`] so that
436/// during code-gen they simply reference the tee output of the first node
437/// instead of creating a redundant `source_stream`.
438#[derive(Debug, Hash, Clone, serde::Serialize)]
439pub enum ClusterMembersState {
440    /// Not yet instantiated.
441    Uninit,
442    /// The primary instance: holds the stream expression and will emit
443    /// `source_stream(expr) -> tee()` during code-gen.
444    Stream(DebugExpr),
445    /// A secondary instance that references the tee output of the primary.
446    /// Stores `(at_location_root, target_cluster_location)` so that `emit_core`
447    /// can derive the deterministic tee ident without extra state.
448    Tee(LocationId, LocationId),
449}
450
451/// A source in a Hydro graph, where data enters the graph.
452#[derive(Debug, Hash, Clone, serde::Serialize)]
453pub enum HydroSource {
454    Stream(DebugExpr),
455    ExternalNetwork(),
456    Iter(DebugExpr),
457    Spin(),
458    ClusterMembers(LocationId, ClusterMembersState),
459    Embedded(#[serde(serialize_with = "serialize_ident")] syn::Ident),
460    EmbeddedSingleton(#[serde(serialize_with = "serialize_ident")] syn::Ident),
461}
462
463#[cfg(feature = "build")]
464/// A trait that abstracts over elements of DFIR code-gen that differ between production deployment
465/// and simulations.
466///
467/// In particular, this lets the simulator fuse together all locations into one DFIR graph, spit
468/// out separate graphs for each tick, and emit hooks for controlling non-deterministic operators.
469pub trait DfirBuilder {
470    /// Whether the representation of singletons should include intermediate states.
471    fn singleton_intermediates(&self) -> bool;
472
473    /// Adds the DFIR statements to the graph for the given location.
474    ///
475    /// The location determines which DFIR graph the statements are placed in (for production,
476    /// the graph of the location's root; for simulation, either the fused async graph or the
477    /// tick's separate graph). In the future (#2902), production codegen will also use the
478    /// location to place tick-located statements inside the tick's `loop { ... }` context.
479    fn add_dfir_at(
480        &mut self,
481        location: &LocationId,
482        dfir: dfir_lang::parse::DfirCode,
483        operator_tag: Option<&str>,
484    );
485
486    /// Emits a source operator (e.g. `source_iter(...)`) that logically lives inside a tick.
487    ///
488    /// * `source_rhs` is the source pipeline (the right-hand side of the `=` assignment, e.g.
489    ///   `source_iter([123])`).
490    /// * `replay_each_tick` selects whether the value is re-emitted on every tick/firing
491    ///   (persisted) or delivered only on the first tick/firing.
492    ///
493    /// The default (simulation) emission places the source directly in the tick's own graph.
494    /// Production overrides this since DFIR sources must be at the root level, outside of the
495    /// tick's `loop { ... }` context: it emits the source at the root and windows it into the
496    /// loop.
497    fn add_tick_source(
498        &mut self,
499        location: &LocationId,
500        source_rhs: TokenStream,
501        out_ident: &syn::Ident,
502        replay_each_tick: bool,
503        operator_tag: Option<&str>,
504    );
505
506    /// The DFIR persistence lifetime for operator state scoped to a single tick, for an operator
507    /// at `op_location`.
508    ///
509    /// Returns `'tick`. In the future (#2902), production codegen will emit tick regions as DFIR
510    /// `loop { ... }` blocks, where this must instead be `'none` when `op_location` is a tick.
511    fn tick_state_lifetime(&self, _op_location: &LocationId) -> TokenStream {
512        quote!('tick)
513    }
514
515    /// The DFIR persistence lifetime for operator state that accumulates across ticks, for an
516    /// operator at `op_location`.
517    ///
518    /// Returns `'static`. In the future (#2902), production codegen will emit tick regions as
519    /// DFIR `loop { ... }` blocks, where this must instead be `'loop` when `op_location` is a
520    /// tick.
521    fn cross_tick_state_lifetime(&self, _op_location: &LocationId) -> TokenStream {
522        quote!('static)
523    }
524
525    #[expect(clippy::too_many_arguments, reason = "TODO")]
526    fn batch(
527        &mut self,
528        in_ident: syn::Ident,
529        in_location: &LocationId,
530        in_kind: &CollectionKind,
531        out_ident: &syn::Ident,
532        out_location: &LocationId,
533        op_meta: &HydroIrOpMetadata,
534        fold_hooked_idents: &HashSet<String>,
535    );
536
537    fn yield_from_tick(
538        &mut self,
539        in_ident: syn::Ident,
540        in_location: &LocationId,
541        in_kind: &CollectionKind,
542        out_ident: &syn::Ident,
543        out_location: &LocationId,
544    );
545
546    /// Un-windows an ident when it is produced inside a tick's `loop { ... }` context
547    /// (`in_location`) but is about to be consumed by an operator emitted at a location
548    /// (`out_location`) outside that loop. Returns the ident to use downstream.
549    ///
550    /// This is needed for operators (e.g. [`HydroNode::ReduceKeyedWatermark`])
551    /// where the generated code must sit inside a logical tick, while the output is not in a
552    /// tick, and so we need this method.
553    fn unwindow_for_consume(
554        &mut self,
555        in_ident: syn::Ident,
556        in_location: &LocationId,
557        out_location: &LocationId,
558    ) -> syn::Ident;
559
560    fn begin_atomic(
561        &mut self,
562        in_ident: syn::Ident,
563        in_location: &LocationId,
564        in_kind: &CollectionKind,
565        out_ident: &syn::Ident,
566        out_location: &LocationId,
567        op_meta: &HydroIrOpMetadata,
568    );
569    fn end_atomic(
570        &mut self,
571        in_ident: syn::Ident,
572        in_location: &LocationId,
573        in_kind: &CollectionKind,
574        out_ident: &syn::Ident,
575    );
576
577    #[expect(clippy::too_many_arguments, reason = "TODO // internal")]
578    fn observe_nondet(
579        &mut self,
580        trusted: bool,
581        location: &LocationId,
582        in_ident: syn::Ident,
583        in_kind: &CollectionKind,
584        out_ident: &syn::Ident,
585        out_kind: &CollectionKind,
586        op_meta: &HydroIrOpMetadata,
587    );
588
589    #[expect(clippy::too_many_arguments, reason = "TODO")]
590    fn merge_ordered(
591        &mut self,
592        location: &LocationId,
593        first_ident: syn::Ident,
594        second_ident: syn::Ident,
595        out_ident: &syn::Ident,
596        in_kind: &CollectionKind,
597        op_meta: &HydroIrOpMetadata,
598        operator_tag: Option<&str>,
599    );
600
601    #[expect(clippy::too_many_arguments, reason = "TODO")]
602    fn create_network(
603        &mut self,
604        from: &LocationId,
605        to: &LocationId,
606        input_ident: syn::Ident,
607        out_ident: &syn::Ident,
608        serialize: Option<&DebugExpr>,
609        sink: syn::Expr,
610        source: syn::Expr,
611        deserialize: Option<&DebugExpr>,
612        external_element_type: Option<&syn::Type>,
613        tag_id: StmtId,
614        networking_info: &crate::networking::NetworkingInfo,
615    );
616
617    fn create_external_source(
618        &mut self,
619        on: &LocationId,
620        source_expr: syn::Expr,
621        out_ident: &syn::Ident,
622        deserialize: Option<&DebugExpr>,
623        tag_id: StmtId,
624    );
625
626    fn create_external_output(
627        &mut self,
628        on: &LocationId,
629        sink_expr: syn::Expr,
630        input_ident: &syn::Ident,
631        serialize: Option<&DebugExpr>,
632        tag_id: StmtId,
633    );
634
635    /// Optionally emit a fold hook that buffers and permutes inputs before the fold.
636    /// Returns the new input ident to use for the fold if a hook was emitted.
637    fn emit_fold_hook(
638        &mut self,
639        location: &LocationId,
640        in_ident: &syn::Ident,
641        in_kind: &CollectionKind,
642        op_meta: &HydroIrOpMetadata,
643    ) -> Option<syn::Ident>;
644
645    /// Inserts necessary code to validate a manual assertion that at this point the
646    /// input live collection is consistent. In production, this is a no-op, but in simulation
647    /// this will (not yet implemented) inject assertions that validate consistency.
648    fn assert_is_consistent(
649        &mut self,
650        trusted: bool,
651        location: &LocationId,
652        in_ident: syn::Ident,
653        out_ident: &syn::Ident,
654    );
655
656    /// Observes non-determinism introduced by a mut closure operating on a non-strict
657    /// (unordered / at-least-once) input. In production this is identity; in simulation
658    /// it delegates to `observe_nondet` with the strict output kind.
659    fn observe_for_mut(
660        &mut self,
661        location: &LocationId,
662        in_ident: syn::Ident,
663        in_kind: &CollectionKind,
664        out_ident: &syn::Ident,
665        op_meta: &HydroIrOpMetadata,
666    );
667
668    fn create_versioned_network_fork(
669        &mut self,
670        channel_id: u32,
671        dest: &LocationId,
672        senders: Vec<(LocationId, syn::Ident, Option<DebugExpr>)>,
673        external_element_type: Option<&syn::Type>,
674        tag_id: StmtId,
675    );
676
677    #[expect(clippy::too_many_arguments, reason = "networking codegen")]
678    fn create_versioned_network(
679        &mut self,
680        channel_id: u32,
681        source: &LocationId,
682        dest: &LocationId,
683        out_ident: &syn::Ident,
684        deserialize: Option<&DebugExpr>,
685        external_element_type: Option<&syn::Type>,
686        tag_id: StmtId,
687    );
688}
689
690/// The production (deployment) DFIR builder: emits one DFIR graph per root location
691/// (process/cluster).
692///
693/// Tick and atomic locations are collapsed onto their root location's graph. In the future
694/// (#2902), this builder will additionally emit each (unified) tick as a root-level
695/// `loop {{ ... }}` context within its root location's graph.
696#[cfg(feature = "build")]
697#[derive(Default)]
698pub struct ProdDfirBuilder {
699    /// The DFIR graph builder for each root location.
700    pub graphs: SecondaryMap<LocationKey, FlatGraphBuilder>,
701    /// The `loop { ... }` context emitted for each (unified) tick location. Keyed by the tick's
702    /// [`LocationId`] (which carries the `ClockId`), so multiple ticks on the same root location
703    /// each get their own sibling root-level loop within that root's graph.
704    tick_loops: HashMap<LocationId, dfir_lang::graph::GraphLoopId>,
705    /// Counter for generating unique intermediate idents at loop boundaries.
706    next_intermediate_id: usize,
707}
708
709#[cfg(feature = "build")]
710impl ProdDfirBuilder {
711    /// Gets the DFIR builder for the given location's root, creating it if necessary.
712    fn graph_mut(&mut self, location: &LocationId) -> &mut FlatGraphBuilder {
713        self.graphs
714            .entry(location.root().key())
715            .expect("location was removed")
716            .or_default()
717    }
718
719    /// Returns the (unified) tick location that `location` belongs to: the location itself for
720    /// tick locations, the wrapped tick for atomic locations, and `None` for root (top-level)
721    /// locations.
722    ///
723    /// Note this is distinct from the module-level `tick_of` (which extracts a raw [`ClockId`]);
724    /// here we need the full [`LocationId`] so it can be used as the loop-map key.
725    fn tick_of(location: &LocationId) -> Option<&LocationId> {
726        match location {
727            LocationId::Tick {
728                tick: Some(_),
729                parent_location: _,
730            } => Some(location),
731            // Tick around atomic: the clock lives in the parent location.
732            LocationId::Tick {
733                tick: None,
734                parent_location,
735            } => Self::tick_of(parent_location),
736            LocationId::Atomic(tick) => Self::tick_of(tick),
737            LocationId::Process(_) | LocationId::Cluster(_) => None,
738        }
739    }
740
741    /// Returns the `loop { ... }` context for the given location, creating it (as a root-level
742    /// loop in the location's root graph) if necessary. Returns `None` for top-level locations.
743    fn loop_context(&mut self, location: &LocationId) -> Option<dfir_lang::graph::GraphLoopId> {
744        let tick_location = Self::tick_of(location)?.clone();
745        if let Some(&loop_id) = self.tick_loops.get(&tick_location) {
746            return Some(loop_id);
747        }
748        let loop_id = self.graph_mut(location).insert_loop(None);
749        self.tick_loops.insert(tick_location, loop_id);
750        Some(loop_id)
751    }
752
753    /// Adds the DFIR statements to the graph for the given location, inside the tick's loop
754    /// context if the location is a tick or atomic location (otherwise at the root level).
755    fn add_dfir_in(
756        &mut self,
757        location: &LocationId,
758        dfir: dfir_lang::parse::DfirCode,
759        operator_tag: Option<&str>,
760    ) {
761        let loop_context = self.loop_context(location);
762        self.graph_mut(location)
763            .add_dfir(dfir, loop_context, operator_tag);
764    }
765
766    /// Generates a unique intermediate identifier for loop-boundary plumbing.
767    fn intermediate_ident(&mut self) -> syn::Ident {
768        let id = self.next_intermediate_id;
769        self.next_intermediate_id += 1;
770        syn::Ident::new(&format!("__loop_boundary_{}", id), Span::call_site())
771    }
772}
773
774#[cfg(feature = "build")]
775impl DfirBuilder for ProdDfirBuilder {
776    fn singleton_intermediates(&self) -> bool {
777        false
778    }
779
780    fn add_dfir_at(
781        &mut self,
782        location: &LocationId,
783        dfir: dfir_lang::parse::DfirCode,
784        operator_tag: Option<&str>,
785    ) {
786        // Place tick/atomic-located statements inside the tick's loop context; everything else
787        // at the root level.
788        self.add_dfir_in(location, dfir, operator_tag);
789    }
790
791    /// In production, DFIR sources must be at the root level, outside of the tick's
792    /// `loop { ... }` context: it emits the source at the root and windows it into the loop.
793    fn add_tick_source(
794        &mut self,
795        location: &LocationId,
796        source_rhs: TokenStream,
797        out_ident: &syn::Ident,
798        replay_each_tick: bool,
799        operator_tag: Option<&str>,
800    ) {
801        // DFIR sources must be at the root level, not inside a `loop { ... }` context. Emit the
802        // source at the root (persisting it there when it should replay every tick, since
803        // `persist` is not permitted inside a loop), then window it into the tick's loop.
804        let source_ident = self.intermediate_ident();
805        let source_stmt = if replay_each_tick {
806            parse_quote! {
807                #source_ident = #source_rhs -> persist::<'static>();
808            }
809        } else {
810            parse_quote! {
811                #source_ident = #source_rhs;
812            }
813        };
814        self.graph_mut(location)
815            .add_dfir(source_stmt, None, operator_tag);
816        self.add_dfir_in(
817            location,
818            parse_quote! {
819                #out_ident = #source_ident -> batch_eager();
820            },
821            operator_tag,
822        );
823    }
824
825    fn batch(
826        &mut self,
827        in_ident: syn::Ident,
828        in_location: &LocationId,
829        in_kind: &CollectionKind,
830        out_ident: &syn::Ident,
831        out_location: &LocationId,
832        _op_meta: &HydroIrOpMetadata,
833        _fold_hooked_idents: &HashSet<String>,
834    ) {
835        let is_singleton_like = matches!(
836            in_kind,
837            CollectionKind::Singleton { .. }
838                | CollectionKind::Optional { .. }
839                | CollectionKind::KeyedSingleton { .. }
840        );
841
842        match (Self::tick_of(in_location), Self::tick_of(out_location)) {
843            (Some(in_tick), Some(out_tick)) => {
844                // Within the same (unified) tick, e.g. entering the tick from its associated
845                // atomic region: both sides live in the same loop.
846                assert_eq!(
847                    in_tick, out_tick,
848                    "batch between distinct ticks should have been unified"
849                );
850                // NOTE(#2902): a bounded singleton-like value produced once inside the
851                // loop (e.g. by `fold_no_replay` in an atomic region) needs to be held across
852                // firings. Operators in atomic regions have `'static` lifetimes so they should
853                // be persisted and replayed properly.
854                self.add_dfir_in(
855                    out_location,
856                    parse_quote! {
857                        #out_ident = #in_ident;
858                    },
859                    None,
860                );
861            }
862            (None, Some(_)) => {
863                // Entering the tick's loop from the top level: emit a windowing operator.
864                // `batch_eager()` preserves the pre-loop tick semantics: the loop fires on every
865                // tick even when the windowed input is empty.
866                let in_ident = if is_singleton_like && in_kind.is_bounded() {
867                    // The bounded value is produced exactly once. Persist it at the root (a
868                    // `loop { ... }` context cannot contain `persist`) so it remains available,
869                    // then window it into the loop on each firing.
870                    let persisted_ident = self.intermediate_ident();
871                    self.add_dfir_in(
872                        in_location,
873                        parse_quote! {
874                            #persisted_ident = #in_ident -> persist::<'static>();
875                        },
876                        None,
877                    );
878                    persisted_ident
879                } else {
880                    in_ident
881                };
882                self.add_dfir_in(
883                    out_location,
884                    parse_quote! {
885                        #out_ident = #in_ident -> batch_eager();
886                    },
887                    None,
888                );
889            }
890            (Some(_), None) | (None, None) => {
891                unreachable!("batch must target a tick location");
892            }
893        }
894    }
895
896    fn yield_from_tick(
897        &mut self,
898        in_ident: syn::Ident,
899        in_location: &LocationId,
900        _in_kind: &CollectionKind,
901        out_ident: &syn::Ident,
902        out_location: &LocationId,
903    ) {
904        // A `YieldConcat` may target either the top level (`latest`/`all_ticks`) or an atomic
905        // region wrapping the *same* tick (`latest_atomic`/`all_ticks_atomic`). The atomic region
906        // is fused with (runs synchronously inside) its tick's loop, so a yield into it stays in
907        // the loop and is emitted as identity — emitting `all_iterations()` there would illegally
908        // exit the loop even though the consumer lives inside it. This mirrors the simulation
909        // builder, which also emits identity for a same-tick atomic yield.
910        //
911        // TODO(#2902 phase 2): singleton/optional yields to the top level need held-state
912        // semantics (observe the latest value *between* firings), not plain event semantics.
913        match (Self::tick_of(in_location), Self::tick_of(out_location)) {
914            (Some(in_tick), Some(out_tick)) => {
915                assert_eq!(
916                    in_tick, out_tick,
917                    "atomic yield to a different tick should have been unified"
918                );
919                self.add_dfir_in(
920                    out_location,
921                    parse_quote! {
922                        #out_ident = #in_ident;
923                    },
924                    None,
925                );
926            }
927            _ => {
928                // Exit the tick's loop back to the top level. `in_ident` is produced inside the
929                // loop; `all_iterations()` is the un-windowing operator, emitted at the root level.
930                self.graph_mut(in_location).add_dfir(
931                    parse_quote! {
932                        #out_ident = #in_ident -> all_iterations();
933                    },
934                    None,
935                    None,
936                );
937            }
938        }
939    }
940
941    fn unwindow_for_consume(
942        &mut self,
943        in_ident: syn::Ident,
944        in_location: &LocationId,
945        out_location: &LocationId,
946    ) -> syn::Ident {
947        // If the input lives inside a tick's loop context but the consumer is emitted outside
948        // that loop, the raw edge would illegally exit the loop. Insert an `all_iterations()`
949        // un-windowing operator (emitted at the root level of the input's graph, just like
950        // `yield_from_tick`) so the boundary crossing is explicit and legal.
951        let in_loop = self.loop_context(in_location);
952        let out_loop = self.loop_context(out_location);
953        if in_loop.is_some() && in_loop != out_loop {
954            let out_ident = self.intermediate_ident();
955            self.graph_mut(in_location).add_dfir(
956                parse_quote! {
957                    #out_ident = #in_ident -> all_iterations();
958                },
959                None,
960                None,
961            );
962            out_ident
963        } else {
964            in_ident
965        }
966    }
967
968    fn begin_atomic(
969        &mut self,
970        in_ident: syn::Ident,
971        in_location: &LocationId,
972        _in_kind: &CollectionKind,
973        out_ident: &syn::Ident,
974        out_location: &LocationId,
975        _op_meta: &HydroIrOpMetadata,
976    ) {
977        // An atomic region is fused with (runs synchronously inside) its tick's loop. Entering it
978        // from the top level windows data in; entering from within the same tick is identity.
979        match (Self::tick_of(in_location), Self::tick_of(out_location)) {
980            (None, Some(_)) => {
981                self.add_dfir_in(
982                    out_location,
983                    parse_quote! {
984                        #out_ident = #in_ident -> batch_eager();
985                    },
986                    None,
987                );
988            }
989            _ => {
990                self.add_dfir_in(
991                    out_location,
992                    parse_quote! {
993                        #out_ident = #in_ident;
994                    },
995                    None,
996                );
997            }
998        }
999    }
1000
1001    fn end_atomic(
1002        &mut self,
1003        in_ident: syn::Ident,
1004        in_location: &LocationId,
1005        _in_kind: &CollectionKind,
1006        out_ident: &syn::Ident,
1007    ) {
1008        // Exit the atomic region (and thus the tick's loop) back to the top level.
1009        self.graph_mut(in_location).add_dfir(
1010            parse_quote! {
1011                #out_ident = #in_ident -> all_iterations();
1012            },
1013            None,
1014            None,
1015        );
1016    }
1017
1018    fn observe_nondet(
1019        &mut self,
1020        _trusted: bool,
1021        location: &LocationId,
1022        in_ident: syn::Ident,
1023        _in_kind: &CollectionKind,
1024        out_ident: &syn::Ident,
1025        _out_kind: &CollectionKind,
1026        _op_meta: &HydroIrOpMetadata,
1027    ) {
1028        let builder = self.graph_mut(location);
1029        builder.add_dfir(
1030            parse_quote! {
1031                #out_ident = #in_ident;
1032            },
1033            None,
1034            None,
1035        );
1036    }
1037
1038    fn merge_ordered(
1039        &mut self,
1040        location: &LocationId,
1041        first_ident: syn::Ident,
1042        second_ident: syn::Ident,
1043        out_ident: &syn::Ident,
1044        _in_kind: &CollectionKind,
1045        _op_meta: &HydroIrOpMetadata,
1046        operator_tag: Option<&str>,
1047    ) {
1048        let builder = self.graph_mut(location);
1049        builder.add_dfir(
1050            parse_quote! {
1051                #out_ident = union();
1052                #first_ident -> [0]#out_ident;
1053                #second_ident -> [1]#out_ident;
1054            },
1055            None,
1056            operator_tag,
1057        );
1058    }
1059
1060    fn create_network(
1061        &mut self,
1062        from: &LocationId,
1063        to: &LocationId,
1064        input_ident: syn::Ident,
1065        out_ident: &syn::Ident,
1066        serialize: Option<&DebugExpr>,
1067        sink: syn::Expr,
1068        source: syn::Expr,
1069        deserialize: Option<&DebugExpr>,
1070        _external_element_type: Option<&syn::Type>,
1071        tag_id: StmtId,
1072        _networking_info: &crate::networking::NetworkingInfo,
1073    ) {
1074        let sender_builder = self.graph_mut(from);
1075        if let Some(serialize_pipeline) = serialize {
1076            sender_builder.add_dfir(
1077                parse_quote! {
1078                    #input_ident -> map(#serialize_pipeline) -> dest_sink(#sink);
1079                },
1080                None,
1081                // operator tag separates send and receive, which otherwise have the same next_stmt_id
1082                Some(&format!("send{}", tag_id)),
1083            );
1084        } else {
1085            sender_builder.add_dfir(
1086                parse_quote! {
1087                    #input_ident -> dest_sink(#sink);
1088                },
1089                None,
1090                Some(&format!("send{}", tag_id)),
1091            );
1092        }
1093
1094        let receiver_builder = self.graph_mut(to);
1095        if let Some(deserialize_pipeline) = deserialize {
1096            receiver_builder.add_dfir(
1097                parse_quote! {
1098                    #out_ident = source_stream(#source) -> map(#deserialize_pipeline);
1099                },
1100                None,
1101                Some(&format!("recv{}", tag_id)),
1102            );
1103        } else {
1104            receiver_builder.add_dfir(
1105                parse_quote! {
1106                    #out_ident = source_stream(#source);
1107                },
1108                None,
1109                Some(&format!("recv{}", tag_id)),
1110            );
1111        }
1112    }
1113
1114    fn create_external_source(
1115        &mut self,
1116        on: &LocationId,
1117        source_expr: syn::Expr,
1118        out_ident: &syn::Ident,
1119        deserialize: Option<&DebugExpr>,
1120        tag_id: StmtId,
1121    ) {
1122        let receiver_builder = self.graph_mut(on);
1123        if let Some(deserialize_pipeline) = deserialize {
1124            receiver_builder.add_dfir(
1125                parse_quote! {
1126                    #out_ident = source_stream(#source_expr) -> map(#deserialize_pipeline);
1127                },
1128                None,
1129                Some(&format!("recv{}", tag_id)),
1130            );
1131        } else {
1132            receiver_builder.add_dfir(
1133                parse_quote! {
1134                    #out_ident = source_stream(#source_expr);
1135                },
1136                None,
1137                Some(&format!("recv{}", tag_id)),
1138            );
1139        }
1140    }
1141
1142    fn create_external_output(
1143        &mut self,
1144        on: &LocationId,
1145        sink_expr: syn::Expr,
1146        input_ident: &syn::Ident,
1147        serialize: Option<&DebugExpr>,
1148        tag_id: StmtId,
1149    ) {
1150        let sender_builder = self.graph_mut(on);
1151        if let Some(serialize_fn) = serialize {
1152            sender_builder.add_dfir(
1153                parse_quote! {
1154                    #input_ident -> map(#serialize_fn) -> dest_sink(#sink_expr);
1155                },
1156                None,
1157                // operator tag separates send and receive, which otherwise have the same next_stmt_id
1158                Some(&format!("send{}", tag_id)),
1159            );
1160        } else {
1161            sender_builder.add_dfir(
1162                parse_quote! {
1163                    #input_ident -> dest_sink(#sink_expr);
1164                },
1165                None,
1166                Some(&format!("send{}", tag_id)),
1167            );
1168        }
1169    }
1170
1171    fn emit_fold_hook(
1172        &mut self,
1173        _location: &LocationId,
1174        _in_ident: &syn::Ident,
1175        _in_kind: &CollectionKind,
1176        _op_meta: &HydroIrOpMetadata,
1177    ) -> Option<syn::Ident> {
1178        None
1179    }
1180
1181    fn assert_is_consistent(
1182        &mut self,
1183        _trusted: bool,
1184        location: &LocationId,
1185        in_ident: syn::Ident,
1186        out_ident: &syn::Ident,
1187    ) {
1188        let builder = self.graph_mut(location);
1189        builder.add_dfir(
1190            parse_quote! {
1191                #out_ident = #in_ident;
1192            },
1193            None,
1194            None,
1195        );
1196    }
1197
1198    fn observe_for_mut(
1199        &mut self,
1200        location: &LocationId,
1201        in_ident: syn::Ident,
1202        _in_kind: &CollectionKind,
1203        out_ident: &syn::Ident,
1204        _op_meta: &HydroIrOpMetadata,
1205    ) {
1206        let builder = self.graph_mut(location);
1207        builder.add_dfir(
1208            parse_quote! {
1209                #out_ident = #in_ident;
1210            },
1211            None,
1212            None,
1213        );
1214    }
1215
1216    fn create_versioned_network_fork(
1217        &mut self,
1218        _channel_id: u32,
1219        _dest: &LocationId,
1220        _senders: Vec<(LocationId, syn::Ident, Option<DebugExpr>)>,
1221        _external_element_type: Option<&syn::Type>,
1222        _tag_id: StmtId,
1223    ) {
1224        unreachable!(
1225            "HydroNode::VersionedNetworkFork is only produced by the multi-version simulator merge \
1226             pass and cannot be emitted by the non-simulation builder"
1227        );
1228    }
1229
1230    fn create_versioned_network(
1231        &mut self,
1232        _channel_id: u32,
1233        _source: &LocationId,
1234        _dest: &LocationId,
1235        _out_ident: &syn::Ident,
1236        _deserialize: Option<&DebugExpr>,
1237        _external_element_type: Option<&syn::Type>,
1238        _tag_id: StmtId,
1239    ) {
1240        unreachable!(
1241            "HydroNode::VersionedNetwork is only produced by the multi-version simulator merge \
1242             pass and cannot be emitted by the non-simulation builder"
1243        );
1244    }
1245}
1246
1247#[cfg(feature = "build")]
1248pub enum BuildersOrCallback<'a, L, N>
1249where
1250    L: FnMut(&mut HydroRoot, &mut crate::Counter<StmtId>),
1251    N: FnMut(&mut HydroNode, &mut crate::Counter<StmtId>),
1252{
1253    Builders(&'a mut dyn DfirBuilder),
1254    Callback(L, N),
1255}
1256
1257/// An root in a Hydro graph, which is an pipeline that doesn't emit
1258/// any downstream values. Traversals over the dataflow graph and
1259/// generating DFIR IR start from roots.
1260#[derive(Debug, Hash, serde::Serialize)]
1261pub enum HydroRoot {
1262    ForEach {
1263        f: ClosureExpr,
1264        input: Box<HydroNode>,
1265        op_metadata: HydroIrOpMetadata,
1266    },
1267    SendExternal {
1268        to_external_key: LocationKey,
1269        to_port_id: ExternalPortId,
1270        to_many: bool,
1271        unpaired: bool,
1272        serialize_fn: Option<DebugExpr>,
1273        instantiate_fn: DebugInstantiate,
1274        input: Box<HydroNode>,
1275        op_metadata: HydroIrOpMetadata,
1276    },
1277    DestSink {
1278        sink: DebugExpr,
1279        input: Box<HydroNode>,
1280        op_metadata: HydroIrOpMetadata,
1281    },
1282    CycleSink {
1283        cycle_id: CycleId,
1284        input: Box<HydroNode>,
1285        op_metadata: HydroIrOpMetadata,
1286    },
1287    EmbeddedOutput {
1288        #[serde(serialize_with = "serialize_ident")]
1289        ident: syn::Ident,
1290        input: Box<HydroNode>,
1291        op_metadata: HydroIrOpMetadata,
1292    },
1293    Null {
1294        input: Box<HydroNode>,
1295        op_metadata: HydroIrOpMetadata,
1296    },
1297}
1298
1299impl HydroRoot {
1300    #[cfg(feature = "build")]
1301    #[expect(clippy::too_many_arguments, reason = "TODO(internal)")]
1302    pub fn compile_network<'a, D>(
1303        &mut self,
1304        extra_stmts: &mut SparseSecondaryMap<LocationKey, Vec<syn::Stmt>>,
1305        seen_tees: &mut SeenSharedNodes,
1306        seen_cluster_members: &mut HashSet<(LocationId, LocationKey)>,
1307        processes: &SparseSecondaryMap<LocationKey, D::Process>,
1308        clusters: &SparseSecondaryMap<LocationKey, D::Cluster>,
1309        externals: &SparseSecondaryMap<LocationKey, D::External>,
1310        env: &mut D::InstantiateEnv,
1311    ) where
1312        D: Deploy<'a>,
1313    {
1314        let refcell_extra_stmts = RefCell::new(extra_stmts);
1315        let refcell_env = RefCell::new(env);
1316        let refcell_seen_cluster_members = RefCell::new(seen_cluster_members);
1317        self.transform_bottom_up(
1318            &mut |l| {
1319                if let HydroRoot::SendExternal {
1320                    #[cfg(feature = "tokio")]
1321                    input,
1322                    #[cfg(feature = "tokio")]
1323                    to_external_key,
1324                    #[cfg(feature = "tokio")]
1325                    to_port_id,
1326                    #[cfg(feature = "tokio")]
1327                    to_many,
1328                    #[cfg(feature = "tokio")]
1329                    unpaired,
1330                    #[cfg(feature = "tokio")]
1331                    instantiate_fn,
1332                    ..
1333                } = l
1334                {
1335                    #[cfg(feature = "tokio")]
1336                    let ((sink_expr, source_expr), connect_fn) = match instantiate_fn {
1337                        DebugInstantiate::Building => {
1338                            let to_node = externals
1339                                .get(*to_external_key)
1340                                .unwrap_or_else(|| {
1341                                    panic!("A external used in the graph was not instantiated: {}", to_external_key)
1342                                })
1343                                .clone();
1344
1345                            match input.metadata().location_id.root() {
1346                                &LocationId::Process(process_key) => {
1347                                    if *to_many {
1348                                        (
1349                                            (
1350                                                D::e2o_many_sink(format!("{}_{}", *to_external_key, *to_port_id)),
1351                                                parse_quote!(DUMMY),
1352                                            ),
1353                                            Box::new(|| {}) as Box<dyn FnOnce()>,
1354                                        )
1355                                    } else {
1356                                        let from_node = processes
1357                                            .get(process_key)
1358                                            .unwrap_or_else(|| {
1359                                                panic!("A process used in the graph was not instantiated: {}", process_key)
1360                                            })
1361                                            .clone();
1362
1363                                        let sink_port = from_node.next_port();
1364                                        let source_port = to_node.next_port();
1365
1366                                        if *unpaired {
1367                                            use stageleft::quote_type;
1368                                            use tokio_util::codec::LengthDelimitedCodec;
1369
1370                                            to_node.register(*to_port_id, source_port.clone());
1371
1372                                            let _ = D::e2o_source(
1373                                                refcell_extra_stmts.borrow_mut().entry(process_key).expect("location was removed").or_default(),
1374                                                &to_node, &source_port,
1375                                                &from_node, &sink_port,
1376                                                &quote_type::<LengthDelimitedCodec>(),
1377                                                format!("{}_{}", *to_external_key, *to_port_id)
1378                                            );
1379                                        }
1380
1381                                        (
1382                                            (
1383                                                D::o2e_sink(
1384                                                    &from_node,
1385                                                    &sink_port,
1386                                                    &to_node,
1387                                                    &source_port,
1388                                                    format!("{}_{}", *to_external_key, *to_port_id)
1389                                                ),
1390                                                parse_quote!(DUMMY),
1391                                            ),
1392                                            if *unpaired {
1393                                                D::e2o_connect(
1394                                                    &to_node,
1395                                                    &source_port,
1396                                                    &from_node,
1397                                                    &sink_port,
1398                                                    *to_many,
1399                                                    NetworkHint::Auto,
1400                                                )
1401                                            } else {
1402                                                Box::new(|| {}) as Box<dyn FnOnce()>
1403                                            },
1404                                        )
1405                                    }
1406                                }
1407                                LocationId::Cluster(cluster_key) => {
1408                                    let from_node = clusters
1409                                        .get(*cluster_key)
1410                                        .unwrap_or_else(|| {
1411                                            panic!("A cluster used in the graph was not instantiated: {}", cluster_key)
1412                                        })
1413                                        .clone();
1414
1415                                    let sink_port = from_node.next_port();
1416                                    let source_port = to_node.next_port();
1417
1418                                    if *unpaired {
1419                                        to_node.register(*to_port_id, source_port.clone());
1420                                    }
1421
1422                                    (
1423                                        (
1424                                            D::m2e_sink(
1425                                                &from_node,
1426                                                &sink_port,
1427                                                &to_node,
1428                                                &source_port,
1429                                                format!("{}_{}", *to_external_key, *to_port_id)
1430                                            ),
1431                                            parse_quote!(DUMMY),
1432                                        ),
1433                                        Box::new(|| {}) as Box<dyn FnOnce()>,
1434                                    )
1435                                }
1436                                _ => panic!()
1437                            }
1438                        },
1439
1440                        DebugInstantiate::Finalized(_) => panic!("network already finalized"),
1441                    };
1442
1443                    #[cfg(not(feature = "tokio"))]
1444                    {
1445                        panic!("Cannot instantiate external inputs without tokio");
1446                    };
1447
1448                    #[cfg(feature = "tokio")]
1449                    {
1450                        *instantiate_fn = DebugInstantiateFinalized {
1451                            sink: sink_expr,
1452                            source: source_expr,
1453                            connect_fn: Some(connect_fn),
1454                        }
1455                        .into();
1456                    };
1457                } else if let HydroRoot::EmbeddedOutput { ident, input, .. } = l {
1458                    let element_type = match &input.metadata().collection_kind {
1459                        CollectionKind::Stream { element_type, .. } => element_type.0.as_ref().clone(),
1460                        _ => panic!("Embedded output must have Stream collection kind"),
1461                    };
1462                    let location_key = match input.metadata().location_id.root() {
1463                        LocationId::Process(key) | LocationId::Cluster(key) => *key,
1464                        _ => panic!("Embedded output must be on a process or cluster"),
1465                    };
1466                    D::register_embedded_output(
1467                        &mut refcell_env.borrow_mut(),
1468                        location_key,
1469                        ident,
1470                        &element_type,
1471                    );
1472                }
1473            },
1474            &mut |n| {
1475                if let HydroNode::Network {
1476                    name,
1477                    networking_info,
1478                    input,
1479                    instantiate_fn,
1480                    serialize,
1481                    deserialize,
1482                    metadata,
1483                    ..
1484                } = n
1485                {
1486                    let external_types = match (
1487                        serialize.external_element_type(),
1488                        deserialize.external_element_type(),
1489                    ) {
1490                        (Some(input_type), Some(output_type)) => Some((input_type, output_type)),
1491                        _ => None,
1492                    };
1493                    let (sink_expr, source_expr, connect_fn) = match instantiate_fn {
1494                        DebugInstantiate::Building => instantiate_network::<D>(
1495                            &mut refcell_env.borrow_mut(),
1496                            input.metadata().location_id.root(),
1497                            metadata.location_id.root(),
1498                            processes,
1499                            clusters,
1500                            name.as_deref(),
1501                            networking_info,
1502                            external_types,
1503                        ),
1504
1505                        DebugInstantiate::Finalized(_) => panic!("network already finalized"),
1506                    };
1507
1508                    *instantiate_fn = DebugInstantiateFinalized {
1509                        sink: sink_expr,
1510                        source: source_expr,
1511                        connect_fn: Some(connect_fn),
1512                    }
1513                    .into();
1514                } else if let HydroNode::ExternalInput {
1515                    from_external_key,
1516                    from_port_id,
1517                    from_many,
1518                    codec_type,
1519                    port_hint,
1520                    instantiate_fn,
1521                    metadata,
1522                    ..
1523                } = n
1524                {
1525                    let ((sink_expr, source_expr), connect_fn) = match instantiate_fn {
1526                        DebugInstantiate::Building => {
1527                            let from_node = externals
1528                                .get(*from_external_key)
1529                                .unwrap_or_else(|| {
1530                                    panic!(
1531                                        "A external used in the graph was not instantiated: {}",
1532                                        from_external_key,
1533                                    )
1534                                })
1535                                .clone();
1536
1537                            match metadata.location_id.root() {
1538                                &LocationId::Process(process_key) => {
1539                                    let to_node = processes
1540                                        .get(process_key)
1541                                        .unwrap_or_else(|| {
1542                                            panic!("A process used in the graph was not instantiated: {}", process_key)
1543                                        })
1544                                        .clone();
1545
1546                                    let sink_port = from_node.next_port();
1547                                    let source_port = to_node.next_port();
1548
1549                                    from_node.register(*from_port_id, sink_port.clone());
1550
1551                                    (
1552                                        (
1553                                            parse_quote!(DUMMY),
1554                                            if *from_many {
1555                                                D::e2o_many_source(
1556                                                    refcell_extra_stmts.borrow_mut().entry(process_key).expect("location was removed").or_default(),
1557                                                    &to_node, &source_port,
1558                                                    codec_type.0.as_ref(),
1559                                                    format!("{}_{}", *from_external_key, *from_port_id)
1560                                                )
1561                                            } else {
1562                                                D::e2o_source(
1563                                                    refcell_extra_stmts.borrow_mut().entry(process_key).expect("location was removed").or_default(),
1564                                                    &from_node, &sink_port,
1565                                                    &to_node, &source_port,
1566                                                    codec_type.0.as_ref(),
1567                                                    format!("{}_{}", *from_external_key, *from_port_id)
1568                                                )
1569                                            },
1570                                        ),
1571                                        D::e2o_connect(&from_node, &sink_port, &to_node, &source_port, *from_many, *port_hint),
1572                                    )
1573                                }
1574                                LocationId::Cluster(cluster_key) => {
1575                                    let to_node = clusters
1576                                        .get(*cluster_key)
1577                                        .unwrap_or_else(|| {
1578                                            panic!("A cluster used in the graph was not instantiated: {}", cluster_key)
1579                                        })
1580                                        .clone();
1581
1582                                    let sink_port = from_node.next_port();
1583                                    let source_port = to_node.next_port();
1584
1585                                    from_node.register(*from_port_id, sink_port.clone());
1586
1587                                    (
1588                                        (
1589                                            parse_quote!(DUMMY),
1590                                            D::e2m_source(
1591                                                refcell_extra_stmts.borrow_mut().entry(*cluster_key).expect("location was removed").or_default(),
1592                                                &from_node, &sink_port,
1593                                                &to_node, &source_port,
1594                                                codec_type.0.as_ref(),
1595                                                format!("{}_{}", *from_external_key, *from_port_id)
1596                                            ),
1597                                        ),
1598                                        D::e2m_connect(&from_node, &sink_port, &to_node, &source_port, *port_hint),
1599                                    )
1600                                }
1601                                _ => panic!()
1602                            }
1603                        },
1604
1605                        DebugInstantiate::Finalized(_) => panic!("network already finalized"),
1606                    };
1607
1608                    *instantiate_fn = DebugInstantiateFinalized {
1609                        sink: sink_expr,
1610                        source: source_expr,
1611                        connect_fn: Some(connect_fn),
1612                    }
1613                    .into();
1614                } else if let HydroNode::Source { source: HydroSource::Embedded(ident), metadata } = n {
1615                    let element_type = match &metadata.collection_kind {
1616                        CollectionKind::Stream { element_type, .. } => element_type.0.as_ref().clone(),
1617                        _ => panic!("Embedded source must have Stream collection kind"),
1618                    };
1619                    let location_key = match metadata.location_id.root() {
1620                        LocationId::Process(key) | LocationId::Cluster(key) => *key,
1621                        _ => panic!("Embedded source must be on a process or cluster"),
1622                    };
1623                    D::register_embedded_stream_input(
1624                        &mut refcell_env.borrow_mut(),
1625                        location_key,
1626                        ident,
1627                        &element_type,
1628                    );
1629                } else if let HydroNode::Source { source: HydroSource::EmbeddedSingleton(ident), metadata } = n {
1630                    let element_type = match &metadata.collection_kind {
1631                        CollectionKind::Singleton { element_type, .. } => element_type.0.as_ref().clone(),
1632                        _ => panic!("EmbeddedSingleton source must have Singleton collection kind"),
1633                    };
1634                    let location_key = match metadata.location_id.root() {
1635                        LocationId::Process(key) | LocationId::Cluster(key) => *key,
1636                        _ => panic!("EmbeddedSingleton source must be on a process or cluster"),
1637                    };
1638                    D::register_embedded_singleton_input(
1639                        &mut refcell_env.borrow_mut(),
1640                        location_key,
1641                        ident,
1642                        &element_type,
1643                    );
1644                } else if let HydroNode::Source { source: HydroSource::ClusterMembers(location_id, state), metadata } = n {
1645                    match state {
1646                        ClusterMembersState::Uninit => {
1647                            let at_location = metadata.location_id.root().clone();
1648                            let key = (at_location.clone(), location_id.key());
1649                            if refcell_seen_cluster_members.borrow_mut().insert(key) {
1650                                // First occurrence: call cluster_membership_stream and mark as Stream.
1651                                let expr = stageleft::QuotedWithContext::splice_untyped_ctx(
1652                                    D::cluster_membership_stream(&mut refcell_env.borrow_mut(), &at_location, location_id),
1653                                    &(),
1654                                );
1655                                *state = ClusterMembersState::Stream(expr.into());
1656                            } else {
1657                                // Already instantiated for this (at, target) pair: just tee.
1658                                *state = ClusterMembersState::Tee(at_location, location_id.clone());
1659                            }
1660                        }
1661                        ClusterMembersState::Stream(_) | ClusterMembersState::Tee(..) => {
1662                            panic!("cluster members already finalized");
1663                        }
1664                    }
1665                }
1666            },
1667            seen_tees,
1668            false,
1669        );
1670    }
1671
1672    pub fn connect_network(&mut self, seen_tees: &mut SeenSharedNodes) {
1673        self.transform_bottom_up(
1674            &mut |l| {
1675                if let HydroRoot::SendExternal { instantiate_fn, .. } = l {
1676                    match instantiate_fn {
1677                        DebugInstantiate::Building => panic!("network not built"),
1678
1679                        DebugInstantiate::Finalized(finalized) => {
1680                            (finalized.connect_fn.take().unwrap())();
1681                        }
1682                    }
1683                }
1684            },
1685            &mut |n| {
1686                if let HydroNode::Network { instantiate_fn, .. }
1687                | HydroNode::ExternalInput { instantiate_fn, .. } = n
1688                {
1689                    match instantiate_fn {
1690                        DebugInstantiate::Building => panic!("network not built"),
1691
1692                        DebugInstantiate::Finalized(finalized) => {
1693                            (finalized.connect_fn.take().unwrap())();
1694                        }
1695                    }
1696                }
1697            },
1698            seen_tees,
1699            false,
1700        );
1701    }
1702
1703    pub fn transform_bottom_up(
1704        &mut self,
1705        transform_root: &mut impl FnMut(&mut HydroRoot),
1706        transform_node: &mut impl FnMut(&mut HydroNode),
1707        seen_tees: &mut SeenSharedNodes,
1708        check_well_formed: bool,
1709    ) {
1710        self.transform_children(
1711            |n, s| n.transform_bottom_up(transform_node, s, check_well_formed),
1712            seen_tees,
1713        );
1714
1715        transform_root(self);
1716    }
1717
1718    pub fn transform_children(
1719        &mut self,
1720        mut transform: impl FnMut(&mut HydroNode, &mut SeenSharedNodes),
1721        seen_tees: &mut SeenSharedNodes,
1722    ) {
1723        match self {
1724            HydroRoot::ForEach { f, input, .. } => {
1725                f.transform_children(&mut transform, seen_tees);
1726                transform(input, seen_tees);
1727            }
1728            HydroRoot::SendExternal { input, .. }
1729            | HydroRoot::DestSink { input, .. }
1730            | HydroRoot::CycleSink { input, .. }
1731            | HydroRoot::EmbeddedOutput { input, .. }
1732            | HydroRoot::Null { input, .. } => {
1733                transform(input, seen_tees);
1734            }
1735        }
1736    }
1737
1738    pub fn deep_clone(&self, seen_tees: &mut SeenSharedNodes) -> HydroRoot {
1739        match self {
1740            HydroRoot::ForEach {
1741                f,
1742                input,
1743                op_metadata,
1744            } => HydroRoot::ForEach {
1745                f: f.deep_clone(seen_tees),
1746                input: Box::new(input.deep_clone(seen_tees)),
1747                op_metadata: op_metadata.clone(),
1748            },
1749            HydroRoot::SendExternal {
1750                to_external_key,
1751                to_port_id,
1752                to_many,
1753                unpaired,
1754                serialize_fn,
1755                instantiate_fn,
1756                input,
1757                op_metadata,
1758            } => HydroRoot::SendExternal {
1759                to_external_key: *to_external_key,
1760                to_port_id: *to_port_id,
1761                to_many: *to_many,
1762                unpaired: *unpaired,
1763                serialize_fn: serialize_fn.clone(),
1764                instantiate_fn: instantiate_fn.clone(),
1765                input: Box::new(input.deep_clone(seen_tees)),
1766                op_metadata: op_metadata.clone(),
1767            },
1768            HydroRoot::DestSink {
1769                sink,
1770                input,
1771                op_metadata,
1772            } => HydroRoot::DestSink {
1773                sink: sink.clone(),
1774                input: Box::new(input.deep_clone(seen_tees)),
1775                op_metadata: op_metadata.clone(),
1776            },
1777            HydroRoot::CycleSink {
1778                cycle_id,
1779                input,
1780                op_metadata,
1781            } => HydroRoot::CycleSink {
1782                cycle_id: *cycle_id,
1783                input: Box::new(input.deep_clone(seen_tees)),
1784                op_metadata: op_metadata.clone(),
1785            },
1786            HydroRoot::EmbeddedOutput {
1787                ident,
1788                input,
1789                op_metadata,
1790            } => HydroRoot::EmbeddedOutput {
1791                ident: ident.clone(),
1792                input: Box::new(input.deep_clone(seen_tees)),
1793                op_metadata: op_metadata.clone(),
1794            },
1795            HydroRoot::Null { input, op_metadata } => HydroRoot::Null {
1796                input: Box::new(input.deep_clone(seen_tees)),
1797                op_metadata: op_metadata.clone(),
1798            },
1799        }
1800    }
1801
1802    #[cfg(feature = "build")]
1803    pub fn emit(
1804        &mut self,
1805        graph_builders: &mut dyn DfirBuilder,
1806        seen_tees: &mut SeenSharedNodes,
1807        built_tees: &mut HashMap<*const RefCell<HydroNode>, Vec<syn::Ident>>,
1808        next_stmt_id: &mut crate::Counter<StmtId>,
1809        fold_hooked_idents: &mut HashSet<String>,
1810    ) {
1811        self.emit_core(
1812            &mut BuildersOrCallback::<
1813                fn(&mut HydroRoot, &mut crate::Counter<StmtId>),
1814                fn(&mut HydroNode, &mut crate::Counter<StmtId>),
1815            >::Builders(graph_builders),
1816            seen_tees,
1817            built_tees,
1818            next_stmt_id,
1819            fold_hooked_idents,
1820        );
1821    }
1822
1823    #[cfg(feature = "build")]
1824    pub fn emit_core(
1825        &mut self,
1826        builders_or_callback: &mut BuildersOrCallback<
1827            '_,
1828            impl FnMut(&mut HydroRoot, &mut crate::Counter<StmtId>),
1829            impl FnMut(&mut HydroNode, &mut crate::Counter<StmtId>),
1830        >,
1831        seen_tees: &mut SeenSharedNodes,
1832        built_tees: &mut HashMap<*const RefCell<HydroNode>, Vec<syn::Ident>>,
1833        next_stmt_id: &mut crate::Counter<StmtId>,
1834        fold_hooked_idents: &mut HashSet<String>,
1835    ) {
1836        match self {
1837            HydroRoot::ForEach { f, input, .. } => {
1838                let input_ident = input.emit_core(
1839                    builders_or_callback,
1840                    seen_tees,
1841                    built_tees,
1842                    next_stmt_id,
1843                    fold_hooked_idents,
1844                );
1845
1846                // for_each is always side-effecting, so we observe non-determinism
1847                // even when the closure does not capture a mut ref (unlike map/filter
1848                // which only observe when they have a mut ref).
1849                let input_ident = if !input.metadata().collection_kind.is_strict() {
1850                    let observe_stmt_id = next_stmt_id.get_and_increment();
1851                    let observe_ident =
1852                        syn::Ident::new(&format!("stream_{}", observe_stmt_id), Span::call_site());
1853                    if let BuildersOrCallback::Builders(graph_builders) = builders_or_callback {
1854                        graph_builders.observe_for_mut(
1855                            &input.metadata().location_id,
1856                            input_ident,
1857                            &input.metadata().collection_kind,
1858                            &observe_ident,
1859                            &input.metadata().op,
1860                        );
1861                    }
1862                    observe_ident
1863                } else {
1864                    input_ident
1865                };
1866
1867                // Emit each captured handoff reference (deduplicated via `built_tees` in the
1868                // `HydroNode::Reference` arm), so that references captured *only* by this
1869                // `for_each` closure are still materialized. This mirrors how node-level
1870                // operators (e.g. `map`) emit their closures' captured references as part of
1871                // their bottom-up traversal. This is done in both the Builders and Callback
1872                // paths so that statement IDs stay consistent between them.
1873                let mut ref_idents = Vec::new();
1874                for (ref_node, _is_mut) in f.singleton_refs.iter_mut() {
1875                    assert!(
1876                        matches!(ref_node, HydroNode::Reference { .. }),
1877                        "singleton_refs should only contain HydroNode::Reference"
1878                    );
1879                    ref_idents.push(ref_node.emit_core(
1880                        builders_or_callback,
1881                        seen_tees,
1882                        built_tees,
1883                        next_stmt_id,
1884                        fold_hooked_idents,
1885                    ));
1886                }
1887
1888                // Mint the root's statement ID only after emitting the captured refs, so that
1889                // statement IDs follow emission order and (in the Callback path) the callback
1890                // observes this root's ID as the most recently allocated one, consistent with
1891                // the other `HydroRoot` variants.
1892                let stmt_id = next_stmt_id.get_and_increment();
1893
1894                match builders_or_callback {
1895                    BuildersOrCallback::Builders(graph_builders) => {
1896                        // The refs' idents are in `singleton_refs` order, matching what
1897                        // `emit_tokens` expects on the ident stack.
1898                        let mut ident_stack: Vec<syn::Ident> = ref_idents;
1899
1900                        let f_tokens = f.emit_tokens(&mut ident_stack);
1901
1902                        graph_builders.add_dfir_at(
1903                            &input.metadata().location_id,
1904                            parse_quote! {
1905                                #input_ident -> for_each(#f_tokens);
1906                            },
1907                            Some(&stmt_id.to_string()),
1908                        );
1909                    }
1910                    BuildersOrCallback::Callback(leaf_callback, _) => {
1911                        leaf_callback(self, next_stmt_id);
1912                    }
1913                }
1914            }
1915
1916            HydroRoot::SendExternal {
1917                serialize_fn,
1918                instantiate_fn,
1919                input,
1920                ..
1921            } => {
1922                let input_ident = input.emit_core(
1923                    builders_or_callback,
1924                    seen_tees,
1925                    built_tees,
1926                    next_stmt_id,
1927                    fold_hooked_idents,
1928                );
1929
1930                let stmt_id = next_stmt_id.get_and_increment();
1931
1932                match builders_or_callback {
1933                    BuildersOrCallback::Builders(graph_builders) => {
1934                        let (sink_expr, _) = match instantiate_fn {
1935                            DebugInstantiate::Building => (
1936                                syn::parse_quote!(DUMMY_SINK),
1937                                syn::parse_quote!(DUMMY_SOURCE),
1938                            ),
1939
1940                            DebugInstantiate::Finalized(finalized) => {
1941                                (finalized.sink.clone(), finalized.source.clone())
1942                            }
1943                        };
1944
1945                        graph_builders.create_external_output(
1946                            &input.metadata().location_id,
1947                            sink_expr,
1948                            &input_ident,
1949                            serialize_fn.as_ref(),
1950                            stmt_id,
1951                        );
1952                    }
1953                    BuildersOrCallback::Callback(leaf_callback, _) => {
1954                        leaf_callback(self, next_stmt_id);
1955                    }
1956                }
1957            }
1958
1959            HydroRoot::DestSink { sink, input, .. } => {
1960                let input_ident = input.emit_core(
1961                    builders_or_callback,
1962                    seen_tees,
1963                    built_tees,
1964                    next_stmt_id,
1965                    fold_hooked_idents,
1966                );
1967
1968                let stmt_id = next_stmt_id.get_and_increment();
1969
1970                match builders_or_callback {
1971                    BuildersOrCallback::Builders(graph_builders) => {
1972                        graph_builders.add_dfir_at(
1973                            &input.metadata().location_id,
1974                            parse_quote! {
1975                                #input_ident -> dest_sink(#sink);
1976                            },
1977                            Some(&stmt_id.to_string()),
1978                        );
1979                    }
1980                    BuildersOrCallback::Callback(leaf_callback, _) => {
1981                        leaf_callback(self, next_stmt_id);
1982                    }
1983                }
1984            }
1985
1986            HydroRoot::CycleSink {
1987                cycle_id, input, ..
1988            } => {
1989                let input_ident = input.emit_core(
1990                    builders_or_callback,
1991                    seen_tees,
1992                    built_tees,
1993                    next_stmt_id,
1994                    fold_hooked_idents,
1995                );
1996
1997                match builders_or_callback {
1998                    BuildersOrCallback::Builders(graph_builders) => {
1999                        let elem_type: syn::Type = match &input.metadata().collection_kind {
2000                            CollectionKind::KeyedSingleton {
2001                                key_type,
2002                                value_type,
2003                                ..
2004                            }
2005                            | CollectionKind::KeyedStream {
2006                                key_type,
2007                                value_type,
2008                                ..
2009                            } => {
2010                                parse_quote!((#key_type, #value_type))
2011                            }
2012                            CollectionKind::Stream { element_type, .. }
2013                            | CollectionKind::Singleton { element_type, .. }
2014                            | CollectionKind::Optional { element_type, .. } => {
2015                                parse_quote!(#element_type)
2016                            }
2017                        };
2018
2019                        let cycle_id_ident = cycle_id.as_ident();
2020                        graph_builders.add_dfir_at(
2021                            &input.metadata().location_id,
2022                            parse_quote! {
2023                                #cycle_id_ident = #input_ident -> identity::<#elem_type>();
2024                            },
2025                            None,
2026                        );
2027                    }
2028                    // No ID, no callback
2029                    BuildersOrCallback::Callback(_, _) => {}
2030                }
2031            }
2032
2033            HydroRoot::EmbeddedOutput { ident, input, .. } => {
2034                let input_ident = input.emit_core(
2035                    builders_or_callback,
2036                    seen_tees,
2037                    built_tees,
2038                    next_stmt_id,
2039                    fold_hooked_idents,
2040                );
2041
2042                let stmt_id = next_stmt_id.get_and_increment();
2043
2044                match builders_or_callback {
2045                    BuildersOrCallback::Builders(graph_builders) => {
2046                        graph_builders.add_dfir_at(
2047                            &input.metadata().location_id,
2048                            parse_quote! {
2049                                #input_ident -> for_each(&mut #ident);
2050                            },
2051                            Some(&stmt_id.to_string()),
2052                        );
2053                    }
2054                    BuildersOrCallback::Callback(leaf_callback, _) => {
2055                        leaf_callback(self, next_stmt_id);
2056                    }
2057                }
2058            }
2059
2060            HydroRoot::Null { input, .. } => {
2061                let input_ident = input.emit_core(
2062                    builders_or_callback,
2063                    seen_tees,
2064                    built_tees,
2065                    next_stmt_id,
2066                    fold_hooked_idents,
2067                );
2068
2069                let stmt_id = next_stmt_id.get_and_increment();
2070
2071                match builders_or_callback {
2072                    BuildersOrCallback::Builders(graph_builders) => {
2073                        graph_builders.add_dfir_at(
2074                            &input.metadata().location_id,
2075                            parse_quote! {
2076                                #input_ident -> for_each(|_| {});
2077                            },
2078                            Some(&stmt_id.to_string()),
2079                        );
2080                    }
2081                    BuildersOrCallback::Callback(leaf_callback, _) => {
2082                        leaf_callback(self, next_stmt_id);
2083                    }
2084                }
2085            }
2086        }
2087    }
2088
2089    pub fn op_metadata(&self) -> &HydroIrOpMetadata {
2090        match self {
2091            HydroRoot::ForEach { op_metadata, .. }
2092            | HydroRoot::SendExternal { op_metadata, .. }
2093            | HydroRoot::DestSink { op_metadata, .. }
2094            | HydroRoot::CycleSink { op_metadata, .. }
2095            | HydroRoot::EmbeddedOutput { op_metadata, .. }
2096            | HydroRoot::Null { op_metadata, .. } => op_metadata,
2097        }
2098    }
2099
2100    pub fn op_metadata_mut(&mut self) -> &mut HydroIrOpMetadata {
2101        match self {
2102            HydroRoot::ForEach { op_metadata, .. }
2103            | HydroRoot::SendExternal { op_metadata, .. }
2104            | HydroRoot::DestSink { op_metadata, .. }
2105            | HydroRoot::CycleSink { op_metadata, .. }
2106            | HydroRoot::EmbeddedOutput { op_metadata, .. }
2107            | HydroRoot::Null { op_metadata, .. } => op_metadata,
2108        }
2109    }
2110
2111    pub fn input(&self) -> &HydroNode {
2112        match self {
2113            HydroRoot::ForEach { input, .. }
2114            | HydroRoot::SendExternal { input, .. }
2115            | HydroRoot::DestSink { input, .. }
2116            | HydroRoot::CycleSink { input, .. }
2117            | HydroRoot::EmbeddedOutput { input, .. }
2118            | HydroRoot::Null { input, .. } => input,
2119        }
2120    }
2121
2122    pub fn input_metadata(&self) -> &HydroIrMetadata {
2123        self.input().metadata()
2124    }
2125
2126    pub fn print_root(&self) -> String {
2127        match self {
2128            HydroRoot::ForEach { f, .. } => format!("ForEach({:?})", f),
2129            HydroRoot::SendExternal { .. } => "SendExternal".to_owned(),
2130            HydroRoot::DestSink { sink, .. } => format!("DestSink({:?})", sink),
2131            HydroRoot::CycleSink { cycle_id, .. } => format!("CycleSink({})", cycle_id),
2132            HydroRoot::EmbeddedOutput { ident, .. } => {
2133                format!("EmbeddedOutput({})", ident)
2134            }
2135            HydroRoot::Null { .. } => "Null".to_owned(),
2136        }
2137    }
2138
2139    pub fn visit_debug_expr(&mut self, mut transform: impl FnMut(&mut DebugExpr)) {
2140        match self {
2141            HydroRoot::ForEach { f, .. } => {
2142                transform(&mut f.expr);
2143            }
2144            HydroRoot::DestSink { sink, .. } => {
2145                transform(sink);
2146            }
2147            HydroRoot::SendExternal { .. }
2148            | HydroRoot::CycleSink { .. }
2149            | HydroRoot::EmbeddedOutput { .. }
2150            | HydroRoot::Null { .. } => {}
2151        }
2152    }
2153}
2154
2155#[cfg(feature = "build")]
2156fn tick_of(loc: &LocationId) -> Option<ClockId> {
2157    match loc {
2158        // Regular tick.
2159        &LocationId::Tick {
2160            tick: Some(tick),
2161            parent_location: _,
2162        } => Some(tick),
2163        // Tick around atomic.
2164        LocationId::Tick {
2165            tick: None,
2166            parent_location,
2167        } => Some(
2168            tick_of(parent_location)
2169                .expect("Tick should have either own clock ID or clock ID within parent_location."),
2170        ),
2171        LocationId::Atomic(inner) => tick_of(inner),
2172        _ => None,
2173    }
2174}
2175
2176#[cfg(feature = "build")]
2177fn remap_location(loc: &mut LocationId, uf: &mut HashMap<ClockId, ClockId>) {
2178    match loc {
2179        LocationId::Tick {
2180            tick,
2181            parent_location,
2182        } => {
2183            if let Some(tick) = tick {
2184                *tick = uf_find(uf, *tick);
2185            }
2186            remap_location(parent_location, uf);
2187        }
2188        LocationId::Atomic(inner) => {
2189            remap_location(inner, uf);
2190        }
2191        LocationId::Process(_) | LocationId::Cluster(_) => {}
2192    }
2193}
2194
2195#[cfg(feature = "build")]
2196fn uf_find(parent: &mut HashMap<ClockId, ClockId>, x: ClockId) -> ClockId {
2197    let p = *parent.get(&x).unwrap_or(&x);
2198    if p == x {
2199        return x;
2200    }
2201    let root = uf_find(parent, p);
2202    parent.insert(x, root);
2203    root
2204}
2205
2206#[cfg(feature = "build")]
2207fn uf_union(parent: &mut HashMap<ClockId, ClockId>, a: ClockId, b: ClockId) {
2208    let ra = uf_find(parent, a);
2209    let rb = uf_find(parent, b);
2210    if ra != rb {
2211        parent.insert(ra, rb);
2212    }
2213}
2214
2215/// Traverse the IR to build a union-find that unifies tick IDs connected
2216/// through `Batch` and `YieldConcat` nodes at atomic boundaries, then
2217/// rewrite all `LocationId`s to use the representative tick ID.
2218#[cfg(feature = "build")]
2219pub fn unify_atomic_ticks(ir: &mut [HydroRoot]) {
2220    let mut uf: HashMap<ClockId, ClockId> = HashMap::new();
2221
2222    // Pass 1: collect unifications.
2223    transform_bottom_up(
2224        ir,
2225        &mut |_| {},
2226        &mut |node: &mut HydroNode| match node {
2227            HydroNode::Batch { inner, metadata } | HydroNode::YieldConcat { inner, metadata } => {
2228                if let (Some(a), Some(b)) = (
2229                    tick_of(&inner.metadata().location_id),
2230                    tick_of(&metadata.location_id),
2231                ) {
2232                    uf_union(&mut uf, a, b);
2233                }
2234            }
2235            HydroNode::Chain {
2236                first,
2237                second,
2238                metadata,
2239            }
2240            | HydroNode::ChainFirst {
2241                first,
2242                second,
2243                metadata,
2244            }
2245            | HydroNode::MergeOrdered {
2246                first,
2247                second,
2248                metadata,
2249            } => {
2250                if let (Some(a), Some(b)) = (
2251                    tick_of(&first.metadata().location_id),
2252                    tick_of(&metadata.location_id),
2253                ) {
2254                    uf_union(&mut uf, a, b);
2255                }
2256                if let (Some(a), Some(b)) = (
2257                    tick_of(&second.metadata().location_id),
2258                    tick_of(&metadata.location_id),
2259                ) {
2260                    uf_union(&mut uf, a, b);
2261                }
2262            }
2263            _ => {}
2264        },
2265        false,
2266    );
2267
2268    // Pass 2: rewrite all LocationIds.
2269    transform_bottom_up(
2270        ir,
2271        &mut |_| {},
2272        &mut |node: &mut HydroNode| {
2273            remap_location(&mut node.metadata_mut().location_id, &mut uf);
2274        },
2275        false,
2276    );
2277}
2278
2279#[cfg(feature = "build")]
2280pub fn emit(ir: &mut Vec<HydroRoot>) -> SecondaryMap<LocationKey, FlatGraphBuilder> {
2281    let mut builders = ProdDfirBuilder::default();
2282    let mut seen_tees = HashMap::new();
2283    let mut built_tees = HashMap::new();
2284    let mut next_stmt_id = crate::Counter::<StmtId>::default();
2285    let mut fold_hooked_idents = HashSet::new();
2286    for leaf in ir {
2287        leaf.emit(
2288            &mut builders,
2289            &mut seen_tees,
2290            &mut built_tees,
2291            &mut next_stmt_id,
2292            &mut fold_hooked_idents,
2293        );
2294    }
2295    builders.graphs
2296}
2297
2298#[cfg(feature = "build")]
2299pub fn traverse_dfir(
2300    ir: &mut [HydroRoot],
2301    transform_root: impl FnMut(&mut HydroRoot, &mut crate::Counter<StmtId>),
2302    transform_node: impl FnMut(&mut HydroNode, &mut crate::Counter<StmtId>),
2303) {
2304    let mut seen_tees = HashMap::new();
2305    let mut built_tees = HashMap::new();
2306    let mut next_stmt_id = crate::Counter::<StmtId>::default();
2307    let mut fold_hooked_idents = HashSet::new();
2308    let mut callback = BuildersOrCallback::Callback(transform_root, transform_node);
2309    ir.iter_mut().for_each(|leaf| {
2310        leaf.emit_core(
2311            &mut callback,
2312            &mut seen_tees,
2313            &mut built_tees,
2314            &mut next_stmt_id,
2315            &mut fold_hooked_idents,
2316        );
2317    });
2318}
2319
2320pub fn transform_bottom_up(
2321    ir: &mut [HydroRoot],
2322    transform_root: &mut impl FnMut(&mut HydroRoot),
2323    transform_node: &mut impl FnMut(&mut HydroNode),
2324    check_well_formed: bool,
2325) {
2326    let mut seen_tees = HashMap::new();
2327    ir.iter_mut().for_each(|leaf| {
2328        leaf.transform_bottom_up(
2329            transform_root,
2330            transform_node,
2331            &mut seen_tees,
2332            check_well_formed,
2333        );
2334    });
2335}
2336
2337pub fn deep_clone(ir: &[HydroRoot]) -> Vec<HydroRoot> {
2338    let mut seen_tees = HashMap::new();
2339    ir.iter()
2340        .map(|leaf| leaf.deep_clone(&mut seen_tees))
2341        .collect()
2342}
2343
2344type PrintedTees = RefCell<Option<(usize, HashMap<*const RefCell<HydroNode>, usize>)>>;
2345thread_local! {
2346    static PRINTED_TEES: PrintedTees = const { RefCell::new(None) };
2347    /// Tracks shared nodes already serialized so that `SharedNode::serialize`
2348    /// emits the full subtree only once and uses a `"<shared N>"` back-reference
2349    /// on subsequent encounters, preventing infinite loops.
2350    static SERIALIZED_SHARED: PrintedTees
2351        = const { RefCell::new(None) };
2352}
2353
2354pub fn dbg_dedup_tee<T>(f: impl FnOnce() -> T) -> T {
2355    PRINTED_TEES.with(|printed_tees| {
2356        let mut printed_tees_mut = printed_tees.borrow_mut();
2357        *printed_tees_mut = Some((0, HashMap::new()));
2358        drop(printed_tees_mut);
2359
2360        let ret = f();
2361
2362        let mut printed_tees_mut = printed_tees.borrow_mut();
2363        *printed_tees_mut = None;
2364
2365        ret
2366    })
2367}
2368
2369/// Runs `f` with a fresh shared-node deduplication scope for serialization.
2370/// Any `SharedNode` serialized inside `f` will be tracked; the first occurrence
2371/// emits the full subtree while later occurrences emit a `{"$shared_ref": id}`
2372/// back-reference.  The tracking state is restored when `f` returns or panics.
2373pub fn serialize_dedup_shared<T>(f: impl FnOnce() -> T) -> T {
2374    let _guard = SerializedSharedGuard::enter();
2375    f()
2376}
2377
2378/// RAII guard that saves/restores the `SERIALIZED_SHARED` thread-local,
2379/// making `serialize_dedup_shared` re-entrant and panic-safe.
2380struct SerializedSharedGuard {
2381    previous: Option<(usize, HashMap<*const RefCell<HydroNode>, usize>)>,
2382}
2383
2384impl SerializedSharedGuard {
2385    fn enter() -> Self {
2386        let previous = SERIALIZED_SHARED.with(|cell| {
2387            let mut guard = cell.borrow_mut();
2388            guard.replace((0, HashMap::new()))
2389        });
2390        Self { previous }
2391    }
2392}
2393
2394impl Drop for SerializedSharedGuard {
2395    fn drop(&mut self) {
2396        SERIALIZED_SHARED.with(|cell| {
2397            *cell.borrow_mut() = self.previous.take();
2398        });
2399    }
2400}
2401
2402pub struct SharedNode(pub Rc<RefCell<HydroNode>>);
2403
2404impl serde::Serialize for SharedNode {
2405    /// Multiple `SharedNode`s can point to the same underlying `HydroNode` (via
2406    /// `Tee` / `Partition`).  A naïve recursive serialization would revisit the
2407    /// same subtree every time and, if the graph ever contains a cycle, loop
2408    /// forever.
2409    ///
2410    /// We keep a thread-local map (`SERIALIZED_SHARED`) from raw `Rc` pointer →
2411    /// integer id.  The first time we see a pointer we assign it the next id and
2412    /// emit the full subtree as `{"$shared": <id>, "node": …}`.  Every later
2413    /// encounter of the same pointer emits `{"$shared_ref": <id>}`, cutting the
2414    /// recursion.  Requires an active `serialize_dedup_shared` scope.
2415    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2416        SERIALIZED_SHARED.with(|cell| {
2417            let mut guard = cell.borrow_mut();
2418            // (next_id, pointer → assigned_id)
2419            let state = guard.as_mut().ok_or_else(|| {
2420                serde::ser::Error::custom(
2421                    "SharedNode serialization requires an active serialize_dedup_shared scope",
2422                )
2423            })?;
2424            let ptr = self.0.as_ptr() as *const RefCell<HydroNode>;
2425
2426            if let Some(&id) = state.1.get(&ptr) {
2427                drop(guard);
2428                use serde::ser::SerializeMap;
2429                let mut map = serializer.serialize_map(Some(1))?;
2430                map.serialize_entry("$shared_ref", &id)?;
2431                map.end()
2432            } else {
2433                let id = state.0;
2434                state.0 += 1;
2435                state.1.insert(ptr, id);
2436                drop(guard);
2437
2438                use serde::ser::SerializeMap;
2439                let mut map = serializer.serialize_map(Some(2))?;
2440                map.serialize_entry("$shared", &id)?;
2441                map.serialize_entry("node", &*self.0.borrow())?;
2442                map.end()
2443            }
2444        })
2445    }
2446}
2447
2448impl SharedNode {
2449    pub fn as_ptr(&self) -> *const RefCell<HydroNode> {
2450        Rc::as_ptr(&self.0)
2451    }
2452}
2453
2454impl Debug for SharedNode {
2455    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2456        PRINTED_TEES.with(|printed_tees| {
2457            let mut printed_tees_mut_borrow = printed_tees.borrow_mut();
2458            let printed_tees_mut = printed_tees_mut_borrow.as_mut();
2459
2460            if let Some(printed_tees_mut) = printed_tees_mut {
2461                if let Some(existing) = printed_tees_mut
2462                    .1
2463                    .get(&(std::ptr::from_ref(self.0.as_ref())))
2464                {
2465                    write!(f, "<shared {}>", existing)
2466                } else {
2467                    let next_id = printed_tees_mut.0;
2468                    printed_tees_mut.0 += 1;
2469                    printed_tees_mut
2470                        .1
2471                        .insert(std::ptr::from_ref(self.0.as_ref()), next_id);
2472                    drop(printed_tees_mut_borrow);
2473                    write!(f, "<shared {}>: ", next_id)?;
2474                    Debug::fmt(&self.0.borrow(), f)
2475                }
2476            } else {
2477                drop(printed_tees_mut_borrow);
2478                write!(f, "<shared>: ")?;
2479                Debug::fmt(&self.0.borrow(), f)
2480            }
2481        })
2482    }
2483}
2484
2485impl Hash for SharedNode {
2486    fn hash<H: Hasher>(&self, state: &mut H) {
2487        self.0.borrow_mut().hash(state);
2488    }
2489}
2490
2491/// A counter for tracking singleton access groups on a `HydroNode::Reference`.
2492///
2493/// Each mutable access increments the counter (before and after) to isolate itself in its own group;
2494/// immutable accesses share the current group.
2495#[derive(Debug)]
2496pub enum AccessCounter {
2497    Counting(Cell<u32>),
2498    Frozen(u32),
2499}
2500
2501impl AccessCounter {
2502    pub fn new() -> Self {
2503        Self::Counting(Cell::new(0))
2504    }
2505
2506    /// Assign the next access group for this reference.
2507    /// Mutable accesses get an isolated group (counter increments before and after).
2508    /// Immutable accesses share the current group.
2509    pub fn next_group(&self, is_mut: bool) -> Self {
2510        let AccessCounter::Counting(count) = self else {
2511            panic!("Cannot count on `AccessCounter::Frozen`");
2512        };
2513        let c = if is_mut {
2514            let c = count.get() + 1;
2515            count.set(c + 1);
2516            c
2517        } else {
2518            count.get()
2519        };
2520        Self::Frozen(c)
2521    }
2522
2523    /// Creates a frozen counter to prevent further counting.
2524    pub fn freeze(&self) -> Self {
2525        Self::Frozen(match self {
2526            Self::Counting(count) => count.get(),
2527            Self::Frozen(count) => *count,
2528        })
2529    }
2530
2531    pub fn frozen_group(&self) -> u32 {
2532        let Self::Frozen(count) = self else {
2533            panic!("`AccessCounter` not frozen");
2534        };
2535        *count
2536    }
2537}
2538
2539impl Default for AccessCounter {
2540    fn default() -> Self {
2541        Self::new()
2542    }
2543}
2544
2545impl Hash for AccessCounter {
2546    fn hash<H: Hasher>(&self, _state: &mut H) {
2547        // Access counter does not participate in hashing — it is runtime bookkeeping.
2548    }
2549}
2550
2551impl serde::Serialize for AccessCounter {
2552    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
2553        let count = match self {
2554            AccessCounter::Counting(count) => count.get(),
2555            AccessCounter::Frozen(count) => *count,
2556        };
2557        count.serialize(serializer)
2558    }
2559}
2560
2561#[derive(serde::Serialize, Clone, PartialEq, Eq, Debug)]
2562pub enum BoundKind {
2563    Unbounded,
2564    Bounded,
2565}
2566
2567#[derive(serde::Serialize, Clone, PartialEq, Eq, Debug)]
2568pub enum OptionalBoundKind {
2569    Unbounded,
2570    /// The optional starts out null, but once it becomes non-null it will remain non-null
2571    /// forever (though the non-null value may change arbitrarily). Erases to [`BoundKind::Unbounded`].
2572    InitNone,
2573    Bounded,
2574}
2575
2576#[derive(serde::Serialize, Clone, PartialEq, Eq, Debug)]
2577pub enum StreamOrder {
2578    NoOrder,
2579    TotalOrder,
2580}
2581
2582#[derive(serde::Serialize, Clone, PartialEq, Eq, Debug)]
2583pub enum StreamRetry {
2584    AtLeastOnce,
2585    ExactlyOnce,
2586}
2587
2588#[derive(serde::Serialize, Clone, PartialEq, Eq, Debug)]
2589pub enum KeyedSingletonBoundKind {
2590    Unbounded,
2591    MonotonicKeys,
2592    MonotonicValue,
2593    BoundedValue,
2594    Bounded,
2595}
2596
2597#[derive(serde::Serialize, Clone, PartialEq, Eq, Debug)]
2598pub enum SingletonBoundKind {
2599    Unbounded,
2600    Monotonic,
2601    Bounded,
2602}
2603
2604#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize)]
2605pub enum CollectionKind {
2606    Stream {
2607        bound: BoundKind,
2608        order: StreamOrder,
2609        retry: StreamRetry,
2610        element_type: DebugType,
2611    },
2612    Singleton {
2613        bound: SingletonBoundKind,
2614        element_type: DebugType,
2615    },
2616    Optional {
2617        bound: OptionalBoundKind,
2618        element_type: DebugType,
2619    },
2620    KeyedStream {
2621        bound: BoundKind,
2622        value_order: StreamOrder,
2623        value_retry: StreamRetry,
2624        key_type: DebugType,
2625        value_type: DebugType,
2626    },
2627    KeyedSingleton {
2628        bound: KeyedSingletonBoundKind,
2629        key_type: DebugType,
2630        value_type: DebugType,
2631    },
2632}
2633
2634impl CollectionKind {
2635    pub fn is_bounded(&self) -> bool {
2636        matches!(
2637            self,
2638            CollectionKind::Stream {
2639                bound: BoundKind::Bounded,
2640                ..
2641            } | CollectionKind::Singleton {
2642                bound: SingletonBoundKind::Bounded,
2643                ..
2644            } | CollectionKind::Optional {
2645                bound: OptionalBoundKind::Bounded,
2646                ..
2647            } | CollectionKind::KeyedStream {
2648                bound: BoundKind::Bounded,
2649                ..
2650            } | CollectionKind::KeyedSingleton {
2651                bound: KeyedSingletonBoundKind::Bounded,
2652                ..
2653            }
2654        )
2655    }
2656
2657    /// Returns whether this collection kind is already "strict" (TotalOrder + ExactlyOnce),
2658    /// meaning no non-determinism needs to be observed for mut closures.
2659    pub fn is_strict(&self) -> bool {
2660        match self {
2661            CollectionKind::Stream { order, retry, .. } => {
2662                *order == StreamOrder::TotalOrder && *retry == StreamRetry::ExactlyOnce
2663            }
2664            CollectionKind::KeyedStream {
2665                value_order,
2666                value_retry,
2667                ..
2668            } => {
2669                *value_order == StreamOrder::TotalOrder && *value_retry == StreamRetry::ExactlyOnce
2670            }
2671            // Singletons/Optionals/KeyedSingletons do not have observable
2672            // non-determinism other than snapshots / batching
2673            CollectionKind::Singleton { .. }
2674            | CollectionKind::Optional { .. }
2675            | CollectionKind::KeyedSingleton { .. } => true,
2676        }
2677    }
2678
2679    /// Creates a "strict" version of this kind with TotalOrder and ExactlyOnce.
2680    pub fn strict_kind(&self) -> CollectionKind {
2681        match self {
2682            CollectionKind::Stream {
2683                bound,
2684                element_type,
2685                ..
2686            } => CollectionKind::Stream {
2687                bound: bound.clone(),
2688                order: StreamOrder::TotalOrder,
2689                retry: StreamRetry::ExactlyOnce,
2690                element_type: element_type.clone(),
2691            },
2692            CollectionKind::KeyedStream {
2693                bound,
2694                key_type,
2695                value_type,
2696                ..
2697            } => CollectionKind::KeyedStream {
2698                bound: bound.clone(),
2699                value_order: StreamOrder::TotalOrder,
2700                value_retry: StreamRetry::ExactlyOnce,
2701                key_type: key_type.clone(),
2702                value_type: value_type.clone(),
2703            },
2704            other => other.clone(),
2705        }
2706    }
2707}
2708
2709#[derive(Clone, serde::Serialize)]
2710pub struct HydroIrMetadata {
2711    pub location_id: LocationId,
2712    pub collection_kind: CollectionKind,
2713    pub consistency: Option<ClusterConsistency>,
2714    pub cardinality: Option<usize>,
2715    pub tag: Option<String>,
2716    pub op: HydroIrOpMetadata,
2717}
2718
2719// HydroIrMetadata shouldn't be used to hash or compare
2720impl Hash for HydroIrMetadata {
2721    fn hash<H: Hasher>(&self, _: &mut H) {}
2722}
2723
2724impl PartialEq for HydroIrMetadata {
2725    fn eq(&self, _: &Self) -> bool {
2726        true
2727    }
2728}
2729
2730impl Eq for HydroIrMetadata {}
2731
2732impl Debug for HydroIrMetadata {
2733    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2734        f.debug_struct("HydroIrMetadata")
2735            .field("location_id", &self.location_id)
2736            .field("collection_kind", &self.collection_kind)
2737            .finish()
2738    }
2739}
2740
2741/// Metadata that is specific to the operator itself, rather than its outputs.
2742/// This is available on _both_ inner nodes and roots.
2743#[derive(Clone, serde::Serialize)]
2744pub struct HydroIrOpMetadata {
2745    #[serde(rename = "span", serialize_with = "serialize_backtrace_as_span")]
2746    pub backtrace: Backtrace,
2747    pub cpu_usage: Option<f64>,
2748    pub network_recv_cpu_usage: Option<f64>,
2749    pub id: Option<usize>,
2750    /// When set, this unsafe operator (e.g. `batch` / `snapshot`) is bound to a simulator
2751    /// hook handle with this ID, letting simulation tests script its decisions. Ignored by
2752    /// non-simulator backends.
2753    #[serde(skip)]
2754    pub sim_hook_id: Option<usize>,
2755}
2756
2757impl HydroIrOpMetadata {
2758    #[expect(
2759        clippy::new_without_default,
2760        reason = "explicit calls to new ensure correct backtrace bounds"
2761    )]
2762    pub fn new() -> HydroIrOpMetadata {
2763        Self::new_with_skip(1)
2764    }
2765
2766    fn new_with_skip(skip_count: usize) -> HydroIrOpMetadata {
2767        HydroIrOpMetadata {
2768            backtrace: Backtrace::get_backtrace(2 + skip_count),
2769            cpu_usage: None,
2770            network_recv_cpu_usage: None,
2771            id: None,
2772            sim_hook_id: None,
2773        }
2774    }
2775}
2776
2777impl Debug for HydroIrOpMetadata {
2778    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2779        f.debug_struct("HydroIrOpMetadata").finish()
2780    }
2781}
2782
2783impl Hash for HydroIrOpMetadata {
2784    fn hash<H: Hasher>(&self, _: &mut H) {}
2785}
2786
2787/// How a network channel's *sender* prepares each message before it is handed to the transport.
2788///
2789/// A channel's serialization is split into a send half ([`NetworkSend`]) and a receive half
2790/// ([`NetworkRecv`]) so that the multi-version simulation merge can reason about each side
2791/// independently (the sender fork and the receiver are separate IR nodes).
2792#[derive(Debug, Clone, Hash, serde::Serialize)]
2793pub enum NetworkSend {
2794    /// Serialization is performed within the Hydro dataflow using the provided serialize
2795    /// expression. This is how channels using [`crate::networking::Bincode`] are lowered.
2796    Custom { serialize_fn: Option<DebugExpr> },
2797    /// Serialization is left to code outside of Hydro (see [`crate::networking::Embedded`]). The
2798    /// raw `element_type` is passed through unserialized; the only transformation is converting a
2799    /// routing [`crate::location::MemberId`] (the destination cluster `tag`, when demuxing) into
2800    /// the raw `TaglessMemberId` used by the transport. Only supported by the embedded backend.
2801    ///
2802    /// Stored as structured info (rather than a pre-baked expression) so that the code can be
2803    /// synthesized in a post-IR codegen pass.
2804    Embedded {
2805        tag: Option<DebugType>,
2806        element_type: DebugType,
2807    },
2808}
2809
2810/// How a network channel's *receiver* recovers each message from the transport. See
2811/// [`NetworkSend`] for the sender half.
2812#[derive(Debug, Clone, Hash, serde::Serialize)]
2813pub enum NetworkRecv {
2814    /// Deserialization is performed within the Hydro dataflow using the provided deserialize
2815    /// expression. This is how channels using [`crate::networking::Bincode`] are lowered.
2816    Custom { deserialize_fn: Option<DebugExpr> },
2817    /// Deserialization is left to code outside of Hydro (see [`crate::networking::Embedded`]). The
2818    /// raw `element_type` is delivered to the receiver directly, with no transport `Result` to
2819    /// unwrap (the external code that produces the stream decides how to handle faults). The only
2820    /// transformation is converting a `TaglessMemberId` back into a typed
2821    /// [`crate::location::MemberId`] (the sender cluster `tag`, when the receiver is keyed by
2822    /// sender). Only supported by the embedded backend.
2823    Embedded {
2824        tag: Option<DebugType>,
2825        element_type: DebugType,
2826    },
2827}
2828
2829#[cfg(feature = "build")]
2830impl NetworkSend {
2831    /// The raw payload type flowing across the channel when serialization is left to external code,
2832    /// or [`None`] when the channel serializes internally.
2833    pub(crate) fn external_element_type(&self) -> Option<&syn::Type> {
2834        match self {
2835            NetworkSend::Custom { .. } => None,
2836            NetworkSend::Embedded { element_type, .. } => Some(&element_type.0),
2837        }
2838    }
2839}
2840
2841#[cfg(feature = "build")]
2842impl NetworkRecv {
2843    /// See [`NetworkSend::external_element_type`].
2844    pub(crate) fn external_element_type(&self) -> Option<&syn::Type> {
2845        match self {
2846            NetworkRecv::Custom { .. } => None,
2847            NetworkRecv::Embedded { element_type, .. } => Some(&element_type.0),
2848        }
2849    }
2850}
2851
2852#[cfg(feature = "build")]
2853impl NetworkSend {
2854    /// The expression applied on the sender to prepare each message for the transport, if any.
2855    pub(crate) fn pipeline(&self) -> Option<DebugExpr> {
2856        match self {
2857            NetworkSend::Custom { serialize_fn } => serialize_fn.clone(),
2858            NetworkSend::Embedded { tag, element_type } => {
2859                let root = crate::staging_util::get_this_crate();
2860                let element_type = &element_type.0;
2861                let expr: syn::Expr = if let Some(tag) = tag {
2862                    let tag = &tag.0;
2863                    parse_quote! {
2864                        #root::runtime_support::stageleft::runtime_support::fn1_type_hint::<(#root::__staged::location::MemberId<#tag>, #element_type), _>(
2865                            |(id, data)| (id.into_tagless(), data)
2866                        )
2867                    }
2868                } else {
2869                    parse_quote! {
2870                        #root::runtime_support::stageleft::runtime_support::fn1_type_hint::<#element_type, _>(
2871                            |data| data
2872                        )
2873                    }
2874                };
2875                Some(expr.into())
2876            }
2877        }
2878    }
2879}
2880
2881#[cfg(feature = "build")]
2882impl NetworkRecv {
2883    /// The expression applied on the receiver to recover each message from the transport, if any.
2884    pub(crate) fn pipeline(&self) -> Option<DebugExpr> {
2885        match self {
2886            NetworkRecv::Custom { deserialize_fn } => deserialize_fn.clone(),
2887            // Embedded channels hand the raw payload to the receiver directly (no transport
2888            // `Result`), so the developer's external code decides how to handle serialization
2889            // faults. The only transformation is restoring the typed `MemberId` when the receiver
2890            // is keyed by the sender.
2891            NetworkRecv::Embedded { tag, .. } => {
2892                let tag = tag.as_ref()?;
2893                let root = crate::staging_util::get_this_crate();
2894                let tag = &tag.0;
2895                let expr: syn::Expr = parse_quote! {
2896                    |(id, b)| (#root::__staged::location::MemberId::<#tag>::from_tagless(id as #root::__staged::location::TaglessMemberId), b)
2897                };
2898                Some(expr.into())
2899            }
2900        }
2901    }
2902}
2903
2904/// An intermediate node in a Hydro graph, which consumes data
2905/// from upstream nodes and emits data to downstream nodes.
2906#[derive(Debug, Hash, serde::Serialize)]
2907pub enum HydroNode {
2908    Placeholder,
2909
2910    /// Manually "casts" between two different collection kinds.
2911    ///
2912    /// Using this IR node requires special care, since it bypasses many of Hydro's core
2913    /// correctness checks. In particular, the user must ensure that every possible
2914    /// "interpretation" of the input corresponds to a distinct "interpretation" of the output,
2915    /// where an "interpretation" is a possible output of `ObserveNonDet` applied to the
2916    /// collection. This ensures that the simulator does not miss any possible outputs.
2917    Cast {
2918        inner: Box<HydroNode>,
2919        metadata: HydroIrMetadata,
2920    },
2921
2922    /// Strengthens the guarantees of a stream by non-deterministically selecting a possible
2923    /// interpretation of the input stream.
2924    ///
2925    /// In production, this simply passes through the input, but in simulation, this operator
2926    /// explicitly selects a randomized interpretation.
2927    ObserveNonDet {
2928        inner: Box<HydroNode>,
2929        trusted: bool, // if true, we do not need to simulate non-determinism
2930        metadata: HydroIrMetadata,
2931    },
2932
2933    Source {
2934        source: HydroSource,
2935        metadata: HydroIrMetadata,
2936    },
2937
2938    SingletonSource {
2939        value: DebugExpr,
2940        first_tick_only: bool,
2941        metadata: HydroIrMetadata,
2942    },
2943
2944    CycleSource {
2945        cycle_id: CycleId,
2946        metadata: HydroIrMetadata,
2947    },
2948
2949    Tee {
2950        inner: SharedNode,
2951        metadata: HydroIrMetadata,
2952    },
2953
2954    /// A reference materialization point. Wraps a SharedNode so that:
2955    /// - The pipe output delivers data to one consumer
2956    /// - `#var` references can borrow the value from the slot
2957    ///
2958    /// In DFIR codegen, emits `ident = inner_ident -> singleton()` or `-> optional()` or
2959    /// `-> handoff()` depending on `kind`.
2960    ///
2961    /// Uses the same `built_tees` dedup pattern as `Tee`.
2962    Reference {
2963        inner: SharedNode,
2964        kind: crate::handoff_ref::HandoffRefKind,
2965        access_counter: AccessCounter,
2966        metadata: HydroIrMetadata,
2967    },
2968
2969    /// An output side of the partition operator.
2970    PartitionSide {
2971        inner: SharedNode,
2972        is_true: bool,
2973        metadata: HydroIrMetadata,
2974    },
2975
2976    /// The inner input of partitioning, shared between two `PartitionSide`.
2977    PartitionShared {
2978        input: Box<HydroNode>,
2979        f: ClosureExpr,
2980        metadata: HydroIrMetadata,
2981    },
2982
2983    BeginAtomic {
2984        inner: Box<HydroNode>,
2985        metadata: HydroIrMetadata,
2986    },
2987
2988    EndAtomic {
2989        inner: Box<HydroNode>,
2990        metadata: HydroIrMetadata,
2991    },
2992
2993    Batch {
2994        inner: Box<HydroNode>,
2995        metadata: HydroIrMetadata,
2996    },
2997
2998    YieldConcat {
2999        inner: Box<HydroNode>,
3000        metadata: HydroIrMetadata,
3001    },
3002
3003    Chain {
3004        first: Box<HydroNode>,
3005        second: Box<HydroNode>,
3006        metadata: HydroIrMetadata,
3007    },
3008
3009    MergeOrdered {
3010        first: Box<HydroNode>,
3011        second: Box<HydroNode>,
3012        metadata: HydroIrMetadata,
3013    },
3014
3015    ChainFirst {
3016        first: Box<HydroNode>,
3017        second: Box<HydroNode>,
3018        metadata: HydroIrMetadata,
3019    },
3020
3021    CrossProduct {
3022        left: Box<HydroNode>,
3023        right: Box<HydroNode>,
3024        metadata: HydroIrMetadata,
3025    },
3026
3027    CrossSingleton {
3028        left: Box<HydroNode>,
3029        right: Box<HydroNode>,
3030        metadata: HydroIrMetadata,
3031    },
3032
3033    Join {
3034        left: Box<HydroNode>,
3035        right: Box<HydroNode>,
3036        metadata: HydroIrMetadata,
3037    },
3038
3039    /// Asymmetric join where the right (build) side is bounded.
3040    /// The build side is accumulated (stratum-delayed) into a hash table,
3041    /// then the left (probe) side streams through preserving its ordering.
3042    JoinHalf {
3043        left: Box<HydroNode>,
3044        right: Box<HydroNode>,
3045        metadata: HydroIrMetadata,
3046    },
3047
3048    Difference {
3049        pos: Box<HydroNode>,
3050        neg: Box<HydroNode>,
3051        metadata: HydroIrMetadata,
3052    },
3053
3054    AntiJoin {
3055        pos: Box<HydroNode>,
3056        neg: Box<HydroNode>,
3057        metadata: HydroIrMetadata,
3058    },
3059
3060    ResolveFutures {
3061        input: Box<HydroNode>,
3062        metadata: HydroIrMetadata,
3063    },
3064    ResolveFuturesBlocking {
3065        input: Box<HydroNode>,
3066        metadata: HydroIrMetadata,
3067    },
3068    ResolveFuturesOrdered {
3069        input: Box<HydroNode>,
3070        metadata: HydroIrMetadata,
3071    },
3072
3073    Map {
3074        f: ClosureExpr,
3075        input: Box<HydroNode>,
3076        metadata: HydroIrMetadata,
3077    },
3078    FlatMap {
3079        f: ClosureExpr,
3080        input: Box<HydroNode>,
3081        metadata: HydroIrMetadata,
3082    },
3083    FlatMapStreamBlocking {
3084        f: ClosureExpr,
3085        input: Box<HydroNode>,
3086        metadata: HydroIrMetadata,
3087    },
3088    Filter {
3089        f: ClosureExpr,
3090        input: Box<HydroNode>,
3091        metadata: HydroIrMetadata,
3092    },
3093    FilterMap {
3094        f: ClosureExpr,
3095        input: Box<HydroNode>,
3096        metadata: HydroIrMetadata,
3097    },
3098
3099    DeferTick {
3100        input: Box<HydroNode>,
3101        metadata: HydroIrMetadata,
3102    },
3103    Enumerate {
3104        input: Box<HydroNode>,
3105        metadata: HydroIrMetadata,
3106    },
3107    Inspect {
3108        f: ClosureExpr,
3109        input: Box<HydroNode>,
3110        metadata: HydroIrMetadata,
3111    },
3112
3113    Unique {
3114        input: Box<HydroNode>,
3115        metadata: HydroIrMetadata,
3116    },
3117
3118    Sort {
3119        input: Box<HydroNode>,
3120        metadata: HydroIrMetadata,
3121    },
3122    Fold {
3123        init: ClosureExpr,
3124        acc: ClosureExpr,
3125        input: Box<HydroNode>,
3126        metadata: HydroIrMetadata,
3127    },
3128
3129    Scan {
3130        init: ClosureExpr,
3131        acc: ClosureExpr,
3132        input: Box<HydroNode>,
3133        metadata: HydroIrMetadata,
3134    },
3135    ScanAsyncBlocking {
3136        init: ClosureExpr,
3137        acc: ClosureExpr,
3138        input: Box<HydroNode>,
3139        metadata: HydroIrMetadata,
3140    },
3141    FoldKeyed {
3142        init: ClosureExpr,
3143        acc: ClosureExpr,
3144        input: Box<HydroNode>,
3145        metadata: HydroIrMetadata,
3146    },
3147
3148    Reduce {
3149        f: ClosureExpr,
3150        input: Box<HydroNode>,
3151        metadata: HydroIrMetadata,
3152    },
3153    ReduceKeyed {
3154        f: ClosureExpr,
3155        input: Box<HydroNode>,
3156        metadata: HydroIrMetadata,
3157    },
3158    ReduceKeyedWatermark {
3159        f: ClosureExpr,
3160        input: Box<HydroNode>,
3161        watermark: Box<HydroNode>,
3162        metadata: HydroIrMetadata,
3163    },
3164
3165    Network {
3166        name: Option<String>,
3167        networking_info: crate::networking::NetworkingInfo,
3168        serialize: NetworkSend,
3169        deserialize: NetworkRecv,
3170        instantiate_fn: DebugInstantiate,
3171        input: Box<HydroNode>,
3172        metadata: HydroIrMetadata,
3173    },
3174
3175    VersionedNetworkFork {
3176        channel_id: u32,
3177        channel_name: String,
3178        senders: Vec<(u32, Box<HydroNode>, NetworkSend)>,
3179        metadata: HydroIrMetadata,
3180    },
3181
3182    VersionedNetwork {
3183        fork: SharedNode,
3184        version: u32,
3185        deserialize: NetworkRecv,
3186        metadata: HydroIrMetadata,
3187    },
3188
3189    ExternalInput {
3190        from_external_key: LocationKey,
3191        from_port_id: ExternalPortId,
3192        from_many: bool,
3193        codec_type: DebugType,
3194        #[serde(skip)]
3195        port_hint: NetworkHint,
3196        instantiate_fn: DebugInstantiate,
3197        deserialize_fn: Option<DebugExpr>,
3198        metadata: HydroIrMetadata,
3199    },
3200
3201    Counter {
3202        tag: String,
3203        duration: DebugExpr,
3204        prefix: String,
3205        input: Box<HydroNode>,
3206        metadata: HydroIrMetadata,
3207    },
3208
3209    AssertIsConsistent {
3210        inner: Box<HydroNode>,
3211        trusted: bool,
3212        metadata: HydroIrMetadata,
3213    },
3214
3215    UnboundSingleton {
3216        inner: Box<HydroNode>,
3217        metadata: HydroIrMetadata,
3218    },
3219}
3220
3221pub type SeenSharedNodes = HashMap<*const RefCell<HydroNode>, Rc<RefCell<HydroNode>>>;
3222pub type SeenSharedNodeLocations = HashMap<*const RefCell<HydroNode>, LocationId>;
3223
3224/// If `f` has a mut singleton ref and `in_kind` is non-strict, emits an
3225/// `observe_for_mut` node and returns the new ident. Otherwise returns
3226/// `in_ident` unchanged. Always consumes a stmt_id when applicable.
3227#[cfg(feature = "build")]
3228fn maybe_observe_for_mut(
3229    f: &ClosureExpr,
3230    in_ident: syn::Ident,
3231    in_location: &LocationId,
3232    in_kind: &CollectionKind,
3233    op_meta: &HydroIrOpMetadata,
3234    builders_or_callback: &mut BuildersOrCallback<
3235        '_,
3236        impl FnMut(&mut HydroRoot, &mut crate::Counter<StmtId>),
3237        impl FnMut(&mut HydroNode, &mut crate::Counter<StmtId>),
3238    >,
3239    next_stmt_id: &mut crate::Counter<StmtId>,
3240) -> syn::Ident {
3241    if f.has_mut_ref() && !in_kind.is_strict() {
3242        let observe_stmt_id = next_stmt_id.get_and_increment();
3243        let observe_ident =
3244            syn::Ident::new(&format!("stream_{}", observe_stmt_id), Span::call_site());
3245        if let BuildersOrCallback::Builders(graph_builders) = builders_or_callback {
3246            graph_builders.observe_for_mut(in_location, in_ident, in_kind, &observe_ident, op_meta);
3247        }
3248        observe_ident
3249    } else {
3250        in_ident
3251    }
3252}
3253
3254impl HydroNode {
3255    pub fn transform_bottom_up(
3256        &mut self,
3257        transform: &mut impl FnMut(&mut HydroNode),
3258        seen_tees: &mut SeenSharedNodes,
3259        check_well_formed: bool,
3260    ) {
3261        self.transform_children(
3262            |n, s| n.transform_bottom_up(transform, s, check_well_formed),
3263            seen_tees,
3264        );
3265
3266        transform(self);
3267
3268        let self_location = self.metadata().location_id.root();
3269
3270        if check_well_formed {
3271            match &*self {
3272                HydroNode::Network { .. } => {}
3273                _ => {
3274                    self.input_metadata().iter().for_each(|i| {
3275                        if i.location_id.root() != self_location {
3276                            panic!(
3277                                "Mismatching IR locations, child: {:?} ({:?}) of: {:?} ({:?})",
3278                                i,
3279                                i.location_id.root(),
3280                                self,
3281                                self_location
3282                            )
3283                        }
3284                    });
3285                }
3286            }
3287        }
3288    }
3289
3290    #[inline(always)]
3291    pub fn transform_children(
3292        &mut self,
3293        mut transform: impl FnMut(&mut HydroNode, &mut SeenSharedNodes),
3294        seen_tees: &mut SeenSharedNodes,
3295    ) {
3296        match self {
3297            HydroNode::Placeholder => {
3298                panic!();
3299            }
3300
3301            HydroNode::Source { .. }
3302            | HydroNode::SingletonSource { .. }
3303            | HydroNode::CycleSource { .. }
3304            | HydroNode::ExternalInput { .. } => {}
3305
3306            HydroNode::Tee { inner, .. } | HydroNode::Reference { inner, .. } => {
3307                if let Some(transformed) = seen_tees.get(&inner.as_ptr()) {
3308                    *inner = SharedNode(transformed.clone());
3309                } else {
3310                    let transformed_cell = Rc::new(RefCell::new(HydroNode::Placeholder));
3311                    seen_tees.insert(inner.as_ptr(), transformed_cell.clone());
3312                    let mut orig = inner.0.replace(HydroNode::Placeholder);
3313                    transform(&mut orig, seen_tees);
3314                    *transformed_cell.borrow_mut() = orig;
3315                    *inner = SharedNode(transformed_cell);
3316                }
3317            }
3318
3319            HydroNode::PartitionSide { inner, .. } => {
3320                if let Some(transformed) = seen_tees.get(&inner.as_ptr()) {
3321                    *inner = SharedNode(transformed.clone());
3322                } else {
3323                    let transformed_cell = Rc::new(RefCell::new(HydroNode::Placeholder));
3324                    seen_tees.insert(inner.as_ptr(), transformed_cell.clone());
3325                    let mut orig: HydroNode = inner.0.replace(HydroNode::Placeholder);
3326                    transform(&mut orig, seen_tees);
3327                    *transformed_cell.borrow_mut() = orig;
3328                    *inner = SharedNode(transformed_cell);
3329                }
3330            }
3331            HydroNode::PartitionShared { input, f, .. } => {
3332                f.transform_children(&mut transform, seen_tees);
3333                transform(input.as_mut(), seen_tees);
3334            }
3335
3336            HydroNode::Cast { inner, .. }
3337            | HydroNode::ObserveNonDet { inner, .. }
3338            | HydroNode::BeginAtomic { inner, .. }
3339            | HydroNode::EndAtomic { inner, .. }
3340            | HydroNode::Batch { inner, .. }
3341            | HydroNode::YieldConcat { inner, .. }
3342            | HydroNode::UnboundSingleton { inner, .. }
3343            | HydroNode::AssertIsConsistent { inner, .. } => {
3344                transform(inner.as_mut(), seen_tees);
3345            }
3346
3347            HydroNode::Chain { first, second, .. } => {
3348                transform(first.as_mut(), seen_tees);
3349                transform(second.as_mut(), seen_tees);
3350            }
3351
3352            HydroNode::MergeOrdered { first, second, .. } => {
3353                transform(first.as_mut(), seen_tees);
3354                transform(second.as_mut(), seen_tees);
3355            }
3356
3357            HydroNode::ChainFirst { first, second, .. } => {
3358                transform(first.as_mut(), seen_tees);
3359                transform(second.as_mut(), seen_tees);
3360            }
3361
3362            HydroNode::CrossSingleton { left, right, .. }
3363            | HydroNode::CrossProduct { left, right, .. }
3364            | HydroNode::Join { left, right, .. }
3365            | HydroNode::JoinHalf { left, right, .. } => {
3366                transform(left.as_mut(), seen_tees);
3367                transform(right.as_mut(), seen_tees);
3368            }
3369
3370            HydroNode::Difference { pos, neg, .. } | HydroNode::AntiJoin { pos, neg, .. } => {
3371                transform(pos.as_mut(), seen_tees);
3372                transform(neg.as_mut(), seen_tees);
3373            }
3374
3375            HydroNode::Map { f, input, .. } => {
3376                f.transform_children(&mut transform, seen_tees);
3377                transform(input.as_mut(), seen_tees);
3378            }
3379            HydroNode::FlatMap { f, input, .. }
3380            | HydroNode::FlatMapStreamBlocking { f, input, .. }
3381            | HydroNode::Filter { f, input, .. }
3382            | HydroNode::FilterMap { f, input, .. }
3383            | HydroNode::Inspect { f, input, .. }
3384            | HydroNode::Reduce { f, input, .. }
3385            | HydroNode::ReduceKeyed { f, input, .. } => {
3386                f.transform_children(&mut transform, seen_tees);
3387                transform(input.as_mut(), seen_tees);
3388            }
3389            HydroNode::ReduceKeyedWatermark {
3390                f,
3391                input,
3392                watermark,
3393                ..
3394            } => {
3395                f.transform_children(&mut transform, seen_tees);
3396                transform(input.as_mut(), seen_tees);
3397                transform(watermark.as_mut(), seen_tees);
3398            }
3399            HydroNode::Fold {
3400                init, acc, input, ..
3401            }
3402            | HydroNode::Scan {
3403                init, acc, input, ..
3404            }
3405            | HydroNode::ScanAsyncBlocking {
3406                init, acc, input, ..
3407            }
3408            | HydroNode::FoldKeyed {
3409                init, acc, input, ..
3410            } => {
3411                init.transform_children(&mut transform, seen_tees);
3412                acc.transform_children(&mut transform, seen_tees);
3413                transform(input.as_mut(), seen_tees);
3414            }
3415            HydroNode::ResolveFutures { input, .. }
3416            | HydroNode::ResolveFuturesBlocking { input, .. }
3417            | HydroNode::ResolveFuturesOrdered { input, .. }
3418            | HydroNode::Sort { input, .. }
3419            | HydroNode::DeferTick { input, .. }
3420            | HydroNode::Enumerate { input, .. }
3421            | HydroNode::Unique { input, .. }
3422            | HydroNode::Network { input, .. }
3423            | HydroNode::Counter { input, .. } => {
3424                transform(input.as_mut(), seen_tees);
3425            }
3426
3427            HydroNode::VersionedNetworkFork { senders, .. } => {
3428                for (_version, sender, _serialize) in senders.iter_mut() {
3429                    transform(sender.as_mut(), seen_tees);
3430                }
3431            }
3432
3433            HydroNode::VersionedNetwork { fork, .. } => {
3434                if let Some(transformed) = seen_tees.get(&fork.as_ptr()) {
3435                    *fork = SharedNode(transformed.clone());
3436                } else {
3437                    let transformed_cell = Rc::new(RefCell::new(HydroNode::Placeholder));
3438                    seen_tees.insert(fork.as_ptr(), transformed_cell.clone());
3439                    let mut orig = fork.0.replace(HydroNode::Placeholder);
3440                    transform(&mut orig, seen_tees);
3441                    *transformed_cell.borrow_mut() = orig;
3442                    *fork = SharedNode(transformed_cell);
3443                }
3444            }
3445        }
3446    }
3447
3448    pub fn deep_clone(&self, seen_tees: &mut SeenSharedNodes) -> HydroNode {
3449        match self {
3450            HydroNode::Placeholder => HydroNode::Placeholder,
3451            HydroNode::Cast { inner, metadata } => HydroNode::Cast {
3452                inner: Box::new(inner.deep_clone(seen_tees)),
3453                metadata: metadata.clone(),
3454            },
3455            HydroNode::UnboundSingleton { inner, metadata } => HydroNode::UnboundSingleton {
3456                inner: Box::new(inner.deep_clone(seen_tees)),
3457                metadata: metadata.clone(),
3458            },
3459            HydroNode::ObserveNonDet {
3460                inner,
3461                trusted,
3462                metadata,
3463            } => HydroNode::ObserveNonDet {
3464                inner: Box::new(inner.deep_clone(seen_tees)),
3465                trusted: *trusted,
3466                metadata: metadata.clone(),
3467            },
3468            HydroNode::AssertIsConsistent {
3469                inner,
3470                trusted,
3471                metadata,
3472            } => HydroNode::AssertIsConsistent {
3473                inner: Box::new(inner.deep_clone(seen_tees)),
3474                trusted: *trusted,
3475                metadata: metadata.clone(),
3476            },
3477            HydroNode::Source { source, metadata } => HydroNode::Source {
3478                source: source.clone(),
3479                metadata: metadata.clone(),
3480            },
3481            HydroNode::SingletonSource {
3482                value,
3483                first_tick_only,
3484                metadata,
3485            } => HydroNode::SingletonSource {
3486                value: value.clone(),
3487                first_tick_only: *first_tick_only,
3488                metadata: metadata.clone(),
3489            },
3490            HydroNode::CycleSource { cycle_id, metadata } => HydroNode::CycleSource {
3491                cycle_id: *cycle_id,
3492                metadata: metadata.clone(),
3493            },
3494            HydroNode::Tee { inner, metadata }
3495            | HydroNode::Reference {
3496                inner, metadata, ..
3497            } => {
3498                let cloned_inner = if let Some(transformed) = seen_tees.get(&inner.as_ptr()) {
3499                    SharedNode(transformed.clone())
3500                } else {
3501                    let new_rc = Rc::new(RefCell::new(HydroNode::Placeholder));
3502                    seen_tees.insert(inner.as_ptr(), new_rc.clone());
3503                    let cloned = inner.0.borrow().deep_clone(seen_tees);
3504                    *new_rc.borrow_mut() = cloned;
3505                    SharedNode(new_rc)
3506                };
3507                if let HydroNode::Reference {
3508                    kind,
3509                    access_counter,
3510                    ..
3511                } = self
3512                {
3513                    HydroNode::Reference {
3514                        inner: cloned_inner,
3515                        kind: *kind,
3516                        access_counter: access_counter.freeze(),
3517                        metadata: metadata.clone(),
3518                    }
3519                } else {
3520                    HydroNode::Tee {
3521                        inner: cloned_inner,
3522                        metadata: metadata.clone(),
3523                    }
3524                }
3525            }
3526            HydroNode::PartitionSide {
3527                inner,
3528                is_true,
3529                metadata,
3530            } => {
3531                if let Some(transformed) = seen_tees.get(&inner.as_ptr()) {
3532                    HydroNode::PartitionSide {
3533                        inner: SharedNode(transformed.clone()),
3534                        is_true: *is_true,
3535                        metadata: metadata.clone(),
3536                    }
3537                } else {
3538                    let new_rc = Rc::new(RefCell::new(HydroNode::Placeholder));
3539                    seen_tees.insert(inner.as_ptr(), new_rc.clone());
3540                    let cloned = inner.0.borrow().deep_clone(seen_tees);
3541                    *new_rc.borrow_mut() = cloned;
3542                    HydroNode::PartitionSide {
3543                        inner: SharedNode(new_rc),
3544                        is_true: *is_true,
3545                        metadata: metadata.clone(),
3546                    }
3547                }
3548            }
3549            HydroNode::PartitionShared { input, f, metadata } => HydroNode::PartitionShared {
3550                input: Box::new(input.deep_clone(seen_tees)),
3551                f: f.deep_clone(seen_tees),
3552                metadata: metadata.clone(),
3553            },
3554            HydroNode::YieldConcat { inner, metadata } => HydroNode::YieldConcat {
3555                inner: Box::new(inner.deep_clone(seen_tees)),
3556                metadata: metadata.clone(),
3557            },
3558            HydroNode::BeginAtomic { inner, metadata } => HydroNode::BeginAtomic {
3559                inner: Box::new(inner.deep_clone(seen_tees)),
3560                metadata: metadata.clone(),
3561            },
3562            HydroNode::EndAtomic { inner, metadata } => HydroNode::EndAtomic {
3563                inner: Box::new(inner.deep_clone(seen_tees)),
3564                metadata: metadata.clone(),
3565            },
3566            HydroNode::Batch { inner, metadata } => HydroNode::Batch {
3567                inner: Box::new(inner.deep_clone(seen_tees)),
3568                metadata: metadata.clone(),
3569            },
3570            HydroNode::Chain {
3571                first,
3572                second,
3573                metadata,
3574            } => HydroNode::Chain {
3575                first: Box::new(first.deep_clone(seen_tees)),
3576                second: Box::new(second.deep_clone(seen_tees)),
3577                metadata: metadata.clone(),
3578            },
3579            HydroNode::MergeOrdered {
3580                first,
3581                second,
3582                metadata,
3583            } => HydroNode::MergeOrdered {
3584                first: Box::new(first.deep_clone(seen_tees)),
3585                second: Box::new(second.deep_clone(seen_tees)),
3586                metadata: metadata.clone(),
3587            },
3588            HydroNode::ChainFirst {
3589                first,
3590                second,
3591                metadata,
3592            } => HydroNode::ChainFirst {
3593                first: Box::new(first.deep_clone(seen_tees)),
3594                second: Box::new(second.deep_clone(seen_tees)),
3595                metadata: metadata.clone(),
3596            },
3597            HydroNode::CrossProduct {
3598                left,
3599                right,
3600                metadata,
3601            } => HydroNode::CrossProduct {
3602                left: Box::new(left.deep_clone(seen_tees)),
3603                right: Box::new(right.deep_clone(seen_tees)),
3604                metadata: metadata.clone(),
3605            },
3606            HydroNode::CrossSingleton {
3607                left,
3608                right,
3609                metadata,
3610            } => HydroNode::CrossSingleton {
3611                left: Box::new(left.deep_clone(seen_tees)),
3612                right: Box::new(right.deep_clone(seen_tees)),
3613                metadata: metadata.clone(),
3614            },
3615            HydroNode::Join {
3616                left,
3617                right,
3618                metadata,
3619            } => HydroNode::Join {
3620                left: Box::new(left.deep_clone(seen_tees)),
3621                right: Box::new(right.deep_clone(seen_tees)),
3622                metadata: metadata.clone(),
3623            },
3624            HydroNode::JoinHalf {
3625                left,
3626                right,
3627                metadata,
3628            } => HydroNode::JoinHalf {
3629                left: Box::new(left.deep_clone(seen_tees)),
3630                right: Box::new(right.deep_clone(seen_tees)),
3631                metadata: metadata.clone(),
3632            },
3633            HydroNode::Difference { pos, neg, metadata } => HydroNode::Difference {
3634                pos: Box::new(pos.deep_clone(seen_tees)),
3635                neg: Box::new(neg.deep_clone(seen_tees)),
3636                metadata: metadata.clone(),
3637            },
3638            HydroNode::AntiJoin { pos, neg, metadata } => HydroNode::AntiJoin {
3639                pos: Box::new(pos.deep_clone(seen_tees)),
3640                neg: Box::new(neg.deep_clone(seen_tees)),
3641                metadata: metadata.clone(),
3642            },
3643            HydroNode::ResolveFutures { input, metadata } => HydroNode::ResolveFutures {
3644                input: Box::new(input.deep_clone(seen_tees)),
3645                metadata: metadata.clone(),
3646            },
3647            HydroNode::ResolveFuturesBlocking { input, metadata } => {
3648                HydroNode::ResolveFuturesBlocking {
3649                    input: Box::new(input.deep_clone(seen_tees)),
3650                    metadata: metadata.clone(),
3651                }
3652            }
3653            HydroNode::ResolveFuturesOrdered { input, metadata } => {
3654                HydroNode::ResolveFuturesOrdered {
3655                    input: Box::new(input.deep_clone(seen_tees)),
3656                    metadata: metadata.clone(),
3657                }
3658            }
3659            HydroNode::Map { f, input, metadata } => HydroNode::Map {
3660                f: f.deep_clone(seen_tees),
3661                input: Box::new(input.deep_clone(seen_tees)),
3662                metadata: metadata.clone(),
3663            },
3664            HydroNode::FlatMap { f, input, metadata } => HydroNode::FlatMap {
3665                f: f.deep_clone(seen_tees),
3666                input: Box::new(input.deep_clone(seen_tees)),
3667                metadata: metadata.clone(),
3668            },
3669            HydroNode::FlatMapStreamBlocking { f, input, metadata } => {
3670                HydroNode::FlatMapStreamBlocking {
3671                    f: f.deep_clone(seen_tees),
3672                    input: Box::new(input.deep_clone(seen_tees)),
3673                    metadata: metadata.clone(),
3674                }
3675            }
3676            HydroNode::Filter { f, input, metadata } => HydroNode::Filter {
3677                f: f.deep_clone(seen_tees),
3678                input: Box::new(input.deep_clone(seen_tees)),
3679                metadata: metadata.clone(),
3680            },
3681            HydroNode::FilterMap { f, input, metadata } => HydroNode::FilterMap {
3682                f: f.deep_clone(seen_tees),
3683                input: Box::new(input.deep_clone(seen_tees)),
3684                metadata: metadata.clone(),
3685            },
3686            HydroNode::DeferTick { input, metadata } => HydroNode::DeferTick {
3687                input: Box::new(input.deep_clone(seen_tees)),
3688                metadata: metadata.clone(),
3689            },
3690            HydroNode::Enumerate { input, metadata } => HydroNode::Enumerate {
3691                input: Box::new(input.deep_clone(seen_tees)),
3692                metadata: metadata.clone(),
3693            },
3694            HydroNode::Inspect { f, input, metadata } => HydroNode::Inspect {
3695                f: f.deep_clone(seen_tees),
3696                input: Box::new(input.deep_clone(seen_tees)),
3697                metadata: metadata.clone(),
3698            },
3699            HydroNode::Unique { input, metadata } => HydroNode::Unique {
3700                input: Box::new(input.deep_clone(seen_tees)),
3701                metadata: metadata.clone(),
3702            },
3703            HydroNode::Sort { input, metadata } => HydroNode::Sort {
3704                input: Box::new(input.deep_clone(seen_tees)),
3705                metadata: metadata.clone(),
3706            },
3707            HydroNode::Fold {
3708                init,
3709                acc,
3710                input,
3711                metadata,
3712            } => HydroNode::Fold {
3713                init: init.deep_clone(seen_tees),
3714                acc: acc.deep_clone(seen_tees),
3715                input: Box::new(input.deep_clone(seen_tees)),
3716                metadata: metadata.clone(),
3717            },
3718            HydroNode::Scan {
3719                init,
3720                acc,
3721                input,
3722                metadata,
3723            } => HydroNode::Scan {
3724                init: init.deep_clone(seen_tees),
3725                acc: acc.deep_clone(seen_tees),
3726                input: Box::new(input.deep_clone(seen_tees)),
3727                metadata: metadata.clone(),
3728            },
3729            HydroNode::ScanAsyncBlocking {
3730                init,
3731                acc,
3732                input,
3733                metadata,
3734            } => HydroNode::ScanAsyncBlocking {
3735                init: init.deep_clone(seen_tees),
3736                acc: acc.deep_clone(seen_tees),
3737                input: Box::new(input.deep_clone(seen_tees)),
3738                metadata: metadata.clone(),
3739            },
3740            HydroNode::FoldKeyed {
3741                init,
3742                acc,
3743                input,
3744                metadata,
3745            } => HydroNode::FoldKeyed {
3746                init: init.deep_clone(seen_tees),
3747                acc: acc.deep_clone(seen_tees),
3748                input: Box::new(input.deep_clone(seen_tees)),
3749                metadata: metadata.clone(),
3750            },
3751            HydroNode::ReduceKeyedWatermark {
3752                f,
3753                input,
3754                watermark,
3755                metadata,
3756            } => HydroNode::ReduceKeyedWatermark {
3757                f: f.deep_clone(seen_tees),
3758                input: Box::new(input.deep_clone(seen_tees)),
3759                watermark: Box::new(watermark.deep_clone(seen_tees)),
3760                metadata: metadata.clone(),
3761            },
3762            HydroNode::Reduce { f, input, metadata } => HydroNode::Reduce {
3763                f: f.deep_clone(seen_tees),
3764                input: Box::new(input.deep_clone(seen_tees)),
3765                metadata: metadata.clone(),
3766            },
3767            HydroNode::ReduceKeyed { f, input, metadata } => HydroNode::ReduceKeyed {
3768                f: f.deep_clone(seen_tees),
3769                input: Box::new(input.deep_clone(seen_tees)),
3770                metadata: metadata.clone(),
3771            },
3772            HydroNode::Network {
3773                name,
3774                networking_info,
3775                serialize,
3776                deserialize,
3777                instantiate_fn,
3778                input,
3779                metadata,
3780            } => HydroNode::Network {
3781                name: name.clone(),
3782                networking_info: networking_info.clone(),
3783                serialize: serialize.clone(),
3784                deserialize: deserialize.clone(),
3785                instantiate_fn: instantiate_fn.clone(),
3786                input: Box::new(input.deep_clone(seen_tees)),
3787                metadata: metadata.clone(),
3788            },
3789            HydroNode::ExternalInput {
3790                from_external_key,
3791                from_port_id,
3792                from_many,
3793                codec_type,
3794                port_hint,
3795                instantiate_fn,
3796                deserialize_fn,
3797                metadata,
3798            } => HydroNode::ExternalInput {
3799                from_external_key: *from_external_key,
3800                from_port_id: *from_port_id,
3801                from_many: *from_many,
3802                codec_type: codec_type.clone(),
3803                port_hint: *port_hint,
3804                instantiate_fn: instantiate_fn.clone(),
3805                deserialize_fn: deserialize_fn.clone(),
3806                metadata: metadata.clone(),
3807            },
3808            HydroNode::Counter {
3809                tag,
3810                duration,
3811                prefix,
3812                input,
3813                metadata,
3814            } => HydroNode::Counter {
3815                tag: tag.clone(),
3816                duration: duration.clone(),
3817                prefix: prefix.clone(),
3818                input: Box::new(input.deep_clone(seen_tees)),
3819                metadata: metadata.clone(),
3820            },
3821            HydroNode::VersionedNetworkFork {
3822                channel_id,
3823                channel_name,
3824                senders,
3825                metadata,
3826            } => HydroNode::VersionedNetworkFork {
3827                channel_id: *channel_id,
3828                channel_name: channel_name.clone(),
3829                senders: senders
3830                    .iter()
3831                    .map(|(version, sender, serialize)| {
3832                        (
3833                            *version,
3834                            Box::new(sender.deep_clone(seen_tees)),
3835                            serialize.clone(),
3836                        )
3837                    })
3838                    .collect(),
3839                metadata: metadata.clone(),
3840            },
3841            HydroNode::VersionedNetwork {
3842                fork,
3843                version,
3844                deserialize,
3845                metadata,
3846            } => {
3847                let cloned_fork = if let Some(transformed) = seen_tees.get(&fork.as_ptr()) {
3848                    SharedNode(transformed.clone())
3849                } else {
3850                    let new_rc = Rc::new(RefCell::new(HydroNode::Placeholder));
3851                    seen_tees.insert(fork.as_ptr(), new_rc.clone());
3852                    let cloned = fork.0.borrow().deep_clone(seen_tees);
3853                    *new_rc.borrow_mut() = cloned;
3854                    SharedNode(new_rc)
3855                };
3856                HydroNode::VersionedNetwork {
3857                    fork: cloned_fork,
3858                    version: *version,
3859                    deserialize: deserialize.clone(),
3860                    metadata: metadata.clone(),
3861                }
3862            }
3863        }
3864    }
3865
3866    #[cfg(feature = "build")]
3867    pub fn emit_core(
3868        &mut self,
3869        builders_or_callback: &mut BuildersOrCallback<
3870            '_,
3871            impl FnMut(&mut HydroRoot, &mut crate::Counter<StmtId>),
3872            impl FnMut(&mut HydroNode, &mut crate::Counter<StmtId>),
3873        >,
3874        seen_tees: &mut SeenSharedNodes,
3875        built_tees: &mut HashMap<*const RefCell<HydroNode>, Vec<syn::Ident>>,
3876        next_stmt_id: &mut crate::Counter<StmtId>,
3877        fold_hooked_idents: &mut HashSet<String>,
3878    ) -> syn::Ident {
3879        let mut ident_stack: Vec<syn::Ident> = Vec::new();
3880
3881        self.transform_bottom_up(
3882            &mut |node: &mut HydroNode| {
3883                let out_location = node.metadata().location_id.clone();
3884                match node {
3885                    HydroNode::Placeholder => {
3886                        panic!()
3887                    }
3888
3889                    HydroNode::Cast { .. } => {
3890                        // Cast passes through the input ident unchanged
3891                        // The input ident is already on the stack from processing the child
3892                        let _ = next_stmt_id.get_and_increment();
3893                        match builders_or_callback {
3894                            BuildersOrCallback::Builders(_) => {}
3895                            BuildersOrCallback::Callback(_, node_callback) => {
3896                                node_callback(node, next_stmt_id);
3897                            }
3898                        }
3899                        // input_ident stays on stack as output
3900                    }
3901
3902                    HydroNode::UnboundSingleton { .. } => {
3903                        let inner_ident = ident_stack.pop().unwrap();
3904
3905                        let stmt_id = next_stmt_id.get_and_increment();
3906                        let out_ident =
3907                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
3908
3909                        match builders_or_callback {
3910                            BuildersOrCallback::Builders(graph_builders) => {
3911                                if graph_builders.singleton_intermediates() {
3912                                    graph_builders.add_dfir_at(
3913                                        &out_location,
3914                                        parse_quote! {
3915                                            #out_ident = #inner_ident;
3916                                        },
3917                                        None,
3918                                    );
3919                                } else {
3920                                    graph_builders.add_dfir_at(
3921                                        &out_location,
3922                                        parse_quote! {
3923                                            #out_ident = #inner_ident -> persist::<'static>();
3924                                        },
3925                                        None,
3926                                    );
3927                                }
3928                            }
3929                            BuildersOrCallback::Callback(_, node_callback) => {
3930                                node_callback(node, next_stmt_id);
3931                            }
3932                        }
3933
3934                        ident_stack.push(out_ident);
3935                    }
3936
3937                    HydroNode::AssertIsConsistent { inner, trusted, .. } => {
3938                        let inner_ident = ident_stack.pop().unwrap();
3939
3940                        let stmt_id = next_stmt_id.get_and_increment();
3941                        let out_ident =
3942                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
3943
3944                        match builders_or_callback {
3945                            BuildersOrCallback::Builders(graph_builders) => {
3946                                graph_builders.assert_is_consistent(
3947                                    *trusted,
3948                                    &inner.metadata().location_id,
3949                                    inner_ident,
3950                                    &out_ident,
3951                                );
3952                            }
3953                            BuildersOrCallback::Callback(_, node_callback) => {
3954                                node_callback(node, next_stmt_id);
3955                            }
3956                        }
3957
3958                        ident_stack.push(out_ident);
3959                    }
3960
3961                    HydroNode::ObserveNonDet {
3962                        inner,
3963                        trusted,
3964                        metadata,
3965                        ..
3966                    } => {
3967                        let inner_ident = ident_stack.pop().unwrap();
3968
3969                        let stmt_id = next_stmt_id.get_and_increment();
3970                        let observe_ident =
3971                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
3972
3973                        match builders_or_callback {
3974                            BuildersOrCallback::Builders(graph_builders) => {
3975                                graph_builders.observe_nondet(
3976                                    *trusted,
3977                                    &inner.metadata().location_id,
3978                                    inner_ident,
3979                                    &inner.metadata().collection_kind,
3980                                    &observe_ident,
3981                                    &metadata.collection_kind,
3982                                    &metadata.op,
3983                                );
3984                            }
3985                            BuildersOrCallback::Callback(_, node_callback) => {
3986                                node_callback(node, next_stmt_id);
3987                            }
3988                        }
3989
3990                        ident_stack.push(observe_ident);
3991                    }
3992
3993                    HydroNode::Batch {
3994                        inner, metadata, ..
3995                    } => {
3996                        let inner_ident = ident_stack.pop().unwrap();
3997
3998                        let stmt_id = next_stmt_id.get_and_increment();
3999                        let batch_ident =
4000                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4001
4002                        match builders_or_callback {
4003                            BuildersOrCallback::Builders(graph_builders) => {
4004                                graph_builders.batch(
4005                                    inner_ident,
4006                                    &inner.metadata().location_id,
4007                                    &inner.metadata().collection_kind,
4008                                    &batch_ident,
4009                                    &out_location,
4010                                    &metadata.op,
4011                                    fold_hooked_idents,
4012                                );
4013                            }
4014                            BuildersOrCallback::Callback(_, node_callback) => {
4015                                node_callback(node, next_stmt_id);
4016                            }
4017                        }
4018
4019                        ident_stack.push(batch_ident);
4020                    }
4021
4022                    HydroNode::YieldConcat { inner, .. } => {
4023                        let inner_ident = ident_stack.pop().unwrap();
4024
4025                        let stmt_id = next_stmt_id.get_and_increment();
4026                        let yield_ident =
4027                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4028
4029                        match builders_or_callback {
4030                            BuildersOrCallback::Builders(graph_builders) => {
4031                                graph_builders.yield_from_tick(
4032                                    inner_ident,
4033                                    &inner.metadata().location_id,
4034                                    &inner.metadata().collection_kind,
4035                                    &yield_ident,
4036                                    &out_location,
4037                                );
4038                            }
4039                            BuildersOrCallback::Callback(_, node_callback) => {
4040                                node_callback(node, next_stmt_id);
4041                            }
4042                        }
4043
4044                        ident_stack.push(yield_ident);
4045                    }
4046
4047                    HydroNode::BeginAtomic { inner, metadata } => {
4048                        let inner_ident = ident_stack.pop().unwrap();
4049
4050                        let stmt_id = next_stmt_id.get_and_increment();
4051                        let begin_ident =
4052                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4053
4054                        match builders_or_callback {
4055                            BuildersOrCallback::Builders(graph_builders) => {
4056                                graph_builders.begin_atomic(
4057                                    inner_ident,
4058                                    &inner.metadata().location_id,
4059                                    &inner.metadata().collection_kind,
4060                                    &begin_ident,
4061                                    &out_location,
4062                                    &metadata.op,
4063                                );
4064                            }
4065                            BuildersOrCallback::Callback(_, node_callback) => {
4066                                node_callback(node, next_stmt_id);
4067                            }
4068                        }
4069
4070                        ident_stack.push(begin_ident);
4071                    }
4072
4073                    HydroNode::EndAtomic { inner, .. } => {
4074                        let inner_ident = ident_stack.pop().unwrap();
4075
4076                        let stmt_id = next_stmt_id.get_and_increment();
4077                        let end_ident =
4078                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4079
4080                        match builders_or_callback {
4081                            BuildersOrCallback::Builders(graph_builders) => {
4082                                graph_builders.end_atomic(
4083                                    inner_ident,
4084                                    &inner.metadata().location_id,
4085                                    &inner.metadata().collection_kind,
4086                                    &end_ident,
4087                                );
4088                            }
4089                            BuildersOrCallback::Callback(_, node_callback) => {
4090                                node_callback(node, next_stmt_id);
4091                            }
4092                        }
4093
4094                        ident_stack.push(end_ident);
4095                    }
4096
4097                    HydroNode::Source {
4098                        source, metadata, ..
4099                    } => {
4100                        if let HydroSource::ExternalNetwork() = source {
4101                            ident_stack.push(syn::Ident::new("DUMMY", Span::call_site()));
4102                        } else {
4103                            let stmt_id = next_stmt_id.get_and_increment();
4104                            let source_ident =
4105                                syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4106
4107                            // For tick-located sources, holds the source pipeline (RHS) so that
4108                            // production codegen can hoist it to the root and window it into the
4109                            // tick's loop (DFIR sources may not live inside a `loop { ... }`).
4110                            let mut tick_source_rhs: Option<TokenStream> = None;
4111
4112                            let source_stmt = match source {
4113                                HydroSource::Stream(expr) => {
4114                                    debug_assert!(metadata.location_id.is_top_level());
4115                                    parse_quote! {
4116                                        #source_ident = source_stream(#expr);
4117                                    }
4118                                }
4119
4120                                HydroSource::ExternalNetwork() => {
4121                                    unreachable!()
4122                                }
4123
4124                                HydroSource::Iter(expr) => {
4125                                    if metadata.location_id.is_root() {
4126                                        parse_quote! {
4127                                            #source_ident = source_iter(#expr);
4128                                        }
4129                                    } else {
4130                                        // Located inside a tick or atomic region (which is fused
4131                                        // into its tick's loop). DFIR sources may not live inside a
4132                                        // `loop { ... }`, so hoist the source to the root and window
4133                                        // it into the loop.
4134                                        // TODO(shadaj): a more natural semantics would be to re-evaluate the expression on each tick
4135                                        tick_source_rhs = Some(quote! { source_iter(#expr) });
4136                                        parse_quote! {
4137                                            #source_ident = source_iter(#expr) -> persist::<'static>();
4138                                        }
4139                                    }
4140                                }
4141
4142                                HydroSource::Spin() => {
4143                                    debug_assert!(metadata.location_id.is_top_level());
4144                                    parse_quote! {
4145                                        #source_ident = spin();
4146                                    }
4147                                }
4148
4149                                HydroSource::ClusterMembers(target_loc, state) => {
4150                                    debug_assert!(metadata.location_id.is_top_level());
4151
4152                                    let members_tee_ident = syn::Ident::new(
4153                                        &format!(
4154                                            "__cluster_members_tee_{}_{}",
4155                                            metadata.location_id.root().key(),
4156                                            target_loc.key(),
4157                                        ),
4158                                        Span::call_site(),
4159                                    );
4160
4161                                    match state {
4162                                        ClusterMembersState::Stream(d) => {
4163                                            parse_quote! {
4164                                                #members_tee_ident = source_stream(#d) -> tee();
4165                                                #source_ident = #members_tee_ident;
4166                                            }
4167                                        },
4168                                        ClusterMembersState::Uninit => syn::parse_quote! {
4169                                            #source_ident = source_stream(DUMMY);
4170                                        },
4171                                        ClusterMembersState::Tee(..) => parse_quote! {
4172                                            #source_ident = #members_tee_ident;
4173                                        },
4174                                    }
4175                                }
4176
4177                                HydroSource::Embedded(ident) => {
4178                                    parse_quote! {
4179                                        #source_ident = source_stream(#ident);
4180                                    }
4181                                }
4182
4183                                HydroSource::EmbeddedSingleton(ident) => {
4184                                    parse_quote! {
4185                                        #source_ident = source_iter([#ident]);
4186                                    }
4187                                }
4188                            };
4189
4190                            match builders_or_callback {
4191                                BuildersOrCallback::Builders(graph_builders) => {
4192                                    if let Some(source_rhs) = tick_source_rhs {
4193                                        graph_builders.add_tick_source(
4194                                            &out_location,
4195                                            source_rhs,
4196                                            &source_ident,
4197                                            true,
4198                                            Some(&stmt_id.to_string()),
4199                                        );
4200                                    } else {
4201                                        graph_builders.add_dfir_at(
4202                                            &out_location,
4203                                            source_stmt,
4204                                            Some(&stmt_id.to_string()),
4205                                        );
4206                                    }
4207                                }
4208                                BuildersOrCallback::Callback(_, node_callback) => {
4209                                    node_callback(node, next_stmt_id);
4210                                }
4211                            }
4212
4213                            ident_stack.push(source_ident);
4214                        }
4215                    }
4216
4217                    HydroNode::SingletonSource { value, first_tick_only, metadata } => {
4218                        let stmt_id = next_stmt_id.get_and_increment();
4219                        let source_ident =
4220                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4221
4222                        match builders_or_callback {
4223                            BuildersOrCallback::Builders(graph_builders) => {
4224                                if *first_tick_only {
4225                                    assert!(
4226                                        !metadata.location_id.is_top_level(),
4227                                        "first_tick_only SingletonSource must be inside a tick"
4228                                    );
4229                                    // Delivered only on the first execution of the tick region.
4230                                    graph_builders.add_tick_source(
4231                                        &out_location,
4232                                        quote! { source_iter([#value]) },
4233                                        &source_ident,
4234                                        false,
4235                                        Some(&stmt_id.to_string()),
4236                                    );
4237                                } else if metadata.location_id.is_root() {
4238                                    if metadata.collection_kind.is_bounded() {
4239                                        graph_builders.add_dfir_at(
4240                                            &out_location,
4241                                            parse_quote! {
4242                                                #source_ident = source_iter([#value]);
4243                                            },
4244                                            Some(&stmt_id.to_string()),
4245                                        );
4246                                    } else {
4247                                        graph_builders.add_dfir_at(
4248                                            &out_location,
4249                                            parse_quote! {
4250                                                #source_ident = source_iter([#value]) -> persist::<'static>();
4251                                            },
4252                                            Some(&stmt_id.to_string()),
4253                                        );
4254                                    }
4255                                } else {
4256                                    // A tick- or atomic-located singleton source (the atomic
4257                                    // region is fused into its tick's loop) that yields its value
4258                                    // on every firing. DFIR sources may not live inside a
4259                                    // `loop { ... }`, so hoist it to the root and window it in.
4260                                    graph_builders.add_tick_source(
4261                                        &out_location,
4262                                        quote! { source_iter([#value]) },
4263                                        &source_ident,
4264                                        true,
4265                                        Some(&stmt_id.to_string()),
4266                                    );
4267                                }
4268                            }
4269                            BuildersOrCallback::Callback(_, node_callback) => {
4270                                node_callback(node, next_stmt_id);
4271                            }
4272                        }
4273
4274                        ident_stack.push(source_ident);
4275                    }
4276
4277                    HydroNode::CycleSource { cycle_id, .. } => {
4278                        let ident = cycle_id.as_ident();
4279
4280                        // consume a stmt id even though we did not emit anything so that we can instrument this
4281                        let _ = next_stmt_id.get_and_increment();
4282
4283                        match builders_or_callback {
4284                            BuildersOrCallback::Builders(_) => {}
4285                            BuildersOrCallback::Callback(_, node_callback) => {
4286                                node_callback(node, next_stmt_id);
4287                            }
4288                        }
4289
4290                        ident_stack.push(ident);
4291                    }
4292
4293                    HydroNode::Tee { inner, .. } => {
4294                        // we consume a stmt id regardless of if we emit the tee() operator,
4295                        // so that during rewrites we touch all recipients of the tee()
4296                        let stmt_id = next_stmt_id.get_and_increment();
4297
4298                        let ret_ident = if let Some(built_idents) =
4299                            built_tees.get(&(std::ptr::from_ref(inner.0.as_ref())))
4300                        {
4301                            match builders_or_callback {
4302                                BuildersOrCallback::Builders(_) => {}
4303                                BuildersOrCallback::Callback(_, node_callback) => {
4304                                    node_callback(node, next_stmt_id);
4305                                }
4306                            }
4307
4308                            built_idents[0].clone()
4309                        } else {
4310                            // The inner node was already processed by transform_bottom_up,
4311                            // so its ident is on the stack
4312                            let inner_ident = ident_stack.pop().unwrap();
4313
4314                            let tee_ident =
4315                                syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4316
4317                            built_tees.insert(
4318                                std::ptr::from_ref(inner.0.as_ref()),
4319                                vec![tee_ident.clone()],
4320                            );
4321
4322                            match builders_or_callback {
4323                                BuildersOrCallback::Builders(graph_builders) => {
4324                                    // NOTE: With `forward_ref`, the fold codegen may not have
4325                                    // run yet when we reach this tee, so `fold_hooked_idents`
4326                                    // might not contain the inner ident. In that case we won't
4327                                    // propagate the "hooked" status to the tee and the
4328                                    // downstream singleton batch will use the normal
4329                                    // `SingletonHook` instead of `PassthroughSingletonHook`.
4330                                    // This is not a soundness issue: the fallback hook still
4331                                    // produces correct behavior, just with a redundant decision
4332                                    // point. TODO(https://github.com/hydro-project/hydro/issues/2856):
4333                                    // fix ordering so forward_ref folds are always processed
4334                                    // before their downstream tees.
4335                                    if fold_hooked_idents.contains(&inner_ident.to_string()) {
4336                                        fold_hooked_idents.insert(tee_ident.to_string());
4337                                    }
4338                                    graph_builders.add_dfir_at(
4339                                        &out_location,
4340                                        parse_quote! {
4341                                            #tee_ident = #inner_ident -> tee();
4342                                        },
4343                                        Some(&stmt_id.to_string()),
4344                                    );
4345                                }
4346                                BuildersOrCallback::Callback(_, node_callback) => {
4347                                    node_callback(node, next_stmt_id);
4348                                }
4349                            }
4350
4351                            tee_ident
4352                        };
4353
4354                        ident_stack.push(ret_ident);
4355                    }
4356
4357                    HydroNode::Reference { inner, kind, .. } => {
4358                        // we consume a stmt id regardless of if we emit the operator,
4359                        // so that during rewrites we touch all recipients
4360                        let stmt_id = next_stmt_id.get_and_increment();
4361
4362                        let ret_ident = if let Some(built_idents) =
4363                            built_tees.get(&(std::ptr::from_ref(inner.0.as_ref())))
4364                        {
4365                            built_idents[0].clone()
4366                        } else {
4367                            let inner_ident = ident_stack.pop().unwrap();
4368
4369                            let ref_ident =
4370                                syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4371
4372                            built_tees.insert(
4373                                std::ptr::from_ref(inner.0.as_ref()),
4374                                vec![ref_ident.clone()],
4375                            );
4376
4377                            match builders_or_callback {
4378                                BuildersOrCallback::Builders(graph_builders) => {
4379                                    let op_ident = syn::Ident::new(
4380                                        match kind {
4381                                            crate::handoff_ref::HandoffRefKind::Singleton => "singleton",
4382                                            crate::handoff_ref::HandoffRefKind::Optional => "optional",
4383                                            crate::handoff_ref::HandoffRefKind::Vec => "handoff",
4384                                        },
4385                                        Span::call_site(),
4386                                    );
4387                                    graph_builders.add_dfir_at(
4388                                        &out_location,
4389                                        parse_quote! {
4390                                            #ref_ident = #inner_ident -> #op_ident();
4391                                        },
4392                                        Some(&stmt_id.to_string()),
4393                                    );
4394                                }
4395                                BuildersOrCallback::Callback(_, node_callback) => {
4396                                    node_callback(node, next_stmt_id);
4397                                }
4398                            }
4399
4400                            ref_ident
4401                        };
4402
4403                        ident_stack.push(ret_ident);
4404                    }
4405
4406                    HydroNode::PartitionSide {
4407                        inner, is_true, metadata: _,
4408                    } => {
4409                        let is_true = *is_true; // need to copy early to avoid borrow checking issues with node
4410                        let ptr = std::ptr::from_ref(inner.0.as_ref());
4411                        let stmt_id = next_stmt_id.get_and_increment();
4412
4413                        let ret_ident = if let Some(built_idents) = built_tees.get(&ptr) {
4414                            match builders_or_callback {
4415                                BuildersOrCallback::Builders(_) => {}
4416                                BuildersOrCallback::Callback(_, node_callback) => {
4417                                    node_callback(node, next_stmt_id);
4418                                }
4419                            }
4420
4421                            let idx = if is_true { 0 } else { 1 };
4422                            built_idents[idx].clone()
4423                        } else {
4424                            // The `PartitionShared` node was already processed by transform_bottom_up,
4425                            // so its ident is on the stack
4426                            let partition_ident = ident_stack.pop().unwrap();
4427
4428                            let true_ident = syn::Ident::new(
4429                                &format!("stream_{}_true", stmt_id),
4430                                Span::call_site(),
4431                            );
4432                            let false_ident = syn::Ident::new(
4433                                &format!("stream_{}_false", stmt_id),
4434                                Span::call_site(),
4435                            );
4436
4437                            built_tees.insert(
4438                                ptr,
4439                                vec![true_ident.clone(), false_ident.clone()],
4440                            );
4441
4442                            let stmt_id = next_stmt_id.get_and_increment();
4443                            match builders_or_callback {
4444                                BuildersOrCallback::Builders(graph_builders) => {
4445                                    graph_builders.add_dfir_at(
4446                                        &out_location,
4447                                        parse_quote! {
4448                                            #true_ident = #partition_ident[0];
4449                                            #false_ident = #partition_ident[1];
4450                                        },
4451                                        Some(&stmt_id.to_string()),
4452                                    );
4453                                }
4454                                BuildersOrCallback::Callback(_, node_callback) => {
4455                                    node_callback(node, next_stmt_id);
4456                                }
4457                            }
4458
4459                            if is_true { true_ident } else { false_ident }
4460                        };
4461
4462                        ident_stack.push(ret_ident);
4463                    }
4464
4465                    HydroNode::PartitionShared { input, f, metadata } => {
4466                        // Pop input ident (pushed last by transform_children) before
4467                        // draining the closure's singleton ref idents below it.
4468                        let inner_ident = ident_stack.pop().unwrap();
4469                        let f_tokens = f.emit_tokens(&mut ident_stack);
4470
4471                        let inner_ident = {
4472                            maybe_observe_for_mut(
4473                                f, inner_ident,
4474                                &input.metadata().location_id,
4475                                &input.metadata().collection_kind,
4476                                &metadata.op,
4477                                builders_or_callback, next_stmt_id,
4478                            )
4479                        };
4480
4481                        let stmt_id = next_stmt_id.get_and_increment();
4482                        let partition_ident = syn::Ident::new(
4483                            &format!("stream_{}_partition", stmt_id),
4484                            Span::call_site(),
4485                        );
4486
4487                        let stmt_id = next_stmt_id.get_and_increment();
4488                        match builders_or_callback {
4489                            BuildersOrCallback::Builders(graph_builders) => {
4490                                graph_builders.add_dfir_at(
4491                                    &out_location,
4492                                    parse_quote! {
4493                                        #partition_ident = #inner_ident -> partition(|__item, __num_outputs| if (#f_tokens)(__item) { 0_usize } else { 1_usize });
4494                                    },
4495                                    Some(&stmt_id.to_string()),
4496                                );
4497                            }
4498                            BuildersOrCallback::Callback(_, node_callback) => {
4499                                node_callback(node, next_stmt_id);
4500                            }
4501                        }
4502                        ident_stack.push(partition_ident);
4503                    }
4504
4505                    HydroNode::Chain { .. } => {
4506                        // Children are processed left-to-right, so second is on top
4507                        let second_ident = ident_stack.pop().unwrap();
4508                        let first_ident = ident_stack.pop().unwrap();
4509
4510                        let stmt_id = next_stmt_id.get_and_increment();
4511                        let chain_ident =
4512                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4513
4514                        match builders_or_callback {
4515                            BuildersOrCallback::Builders(graph_builders) => {
4516                                graph_builders.add_dfir_at(
4517                                    &out_location,
4518                                    parse_quote! {
4519                                        #chain_ident = chain();
4520                                        #first_ident -> [0]#chain_ident;
4521                                        #second_ident -> [1]#chain_ident;
4522                                    },
4523                                    Some(&stmt_id.to_string()),
4524                                );
4525                            }
4526                            BuildersOrCallback::Callback(_, node_callback) => {
4527                                node_callback(node, next_stmt_id);
4528                            }
4529                        }
4530
4531                        ident_stack.push(chain_ident);
4532                    }
4533
4534                    HydroNode::MergeOrdered { first, metadata, .. } => {
4535                        let second_ident = ident_stack.pop().unwrap();
4536                        let first_ident = ident_stack.pop().unwrap();
4537
4538                        let stmt_id = next_stmt_id.get_and_increment();
4539                        let merge_ident =
4540                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4541
4542                        match builders_or_callback {
4543                            BuildersOrCallback::Builders(graph_builders) => {
4544                                graph_builders.merge_ordered(
4545                                    &first.metadata().location_id,
4546                                    first_ident,
4547                                    second_ident,
4548                                    &merge_ident,
4549                                    &first.metadata().collection_kind,
4550                                    &metadata.op,
4551                                    Some(&stmt_id.to_string()),
4552                                );
4553                            }
4554                            BuildersOrCallback::Callback(_, node_callback) => {
4555                                node_callback(node, next_stmt_id);
4556                            }
4557                        }
4558
4559                        ident_stack.push(merge_ident);
4560                    }
4561
4562                    HydroNode::ChainFirst { .. } => {
4563                        let second_ident = ident_stack.pop().unwrap();
4564                        let first_ident = ident_stack.pop().unwrap();
4565
4566                        let stmt_id = next_stmt_id.get_and_increment();
4567                        let chain_ident =
4568                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4569
4570                        match builders_or_callback {
4571                            BuildersOrCallback::Builders(graph_builders) => {
4572                                graph_builders.add_dfir_at(
4573                                    &out_location,
4574                                    parse_quote! {
4575                                        #chain_ident = chain_first_n(1);
4576                                        #first_ident -> [0]#chain_ident;
4577                                        #second_ident -> [1]#chain_ident;
4578                                    },
4579                                    Some(&stmt_id.to_string()),
4580                                );
4581                            }
4582                            BuildersOrCallback::Callback(_, node_callback) => {
4583                                node_callback(node, next_stmt_id);
4584                            }
4585                        }
4586
4587                        ident_stack.push(chain_ident);
4588                    }
4589
4590                    HydroNode::CrossSingleton { right, .. } => {
4591                        let right_ident = ident_stack.pop().unwrap();
4592                        let left_ident = ident_stack.pop().unwrap();
4593
4594                        let stmt_id = next_stmt_id.get_and_increment();
4595                        let cross_ident =
4596                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4597
4598                        match builders_or_callback {
4599                            BuildersOrCallback::Builders(graph_builders) => {
4600                                if right.metadata().location_id.is_top_level()
4601                                    && right.metadata().collection_kind.is_bounded()
4602                                {
4603                                    let lifetime =
4604                                        graph_builders.cross_tick_state_lifetime(&out_location);
4605                                    graph_builders.add_dfir_at(
4606                                        &out_location,
4607                                        parse_quote! {
4608                                            #cross_ident = cross_singleton::<#lifetime>();
4609                                            #left_ident -> [input]#cross_ident;
4610                                            #right_ident -> [single]#cross_ident;
4611                                        },
4612                                        Some(&stmt_id.to_string()),
4613                                    );
4614                                } else {
4615                                    graph_builders.add_dfir_at(
4616                                        &out_location,
4617                                        parse_quote! {
4618                                            #cross_ident = cross_singleton();
4619                                            #left_ident -> [input]#cross_ident;
4620                                            #right_ident -> [single]#cross_ident;
4621                                        },
4622                                        Some(&stmt_id.to_string()),
4623                                    );
4624                                }
4625                            }
4626                            BuildersOrCallback::Callback(_, node_callback) => {
4627                                node_callback(node, next_stmt_id);
4628                            }
4629                        }
4630
4631                        ident_stack.push(cross_ident);
4632                    }
4633
4634                    HydroNode::CrossProduct { .. } | HydroNode::Join { .. } => {
4635                        let operator: syn::Ident = if matches!(node, HydroNode::CrossProduct { .. }) {
4636                            parse_quote!(cross_join_multiset)
4637                        } else {
4638                            parse_quote!(join_multiset)
4639                        };
4640
4641                        let (HydroNode::CrossProduct { left, right, .. }
4642                        | HydroNode::Join { left, right, .. }) = node
4643                        else {
4644                            unreachable!()
4645                        };
4646
4647                        let is_top_level = left.metadata().location_id.is_top_level()
4648                            && right.metadata().location_id.is_top_level();
4649                        let left_top_level = left.metadata().location_id.is_top_level();
4650                        let right_top_level = right.metadata().location_id.is_top_level();
4651
4652                        let right_ident = ident_stack.pop().unwrap();
4653                        let left_ident = ident_stack.pop().unwrap();
4654
4655                        let stmt_id = next_stmt_id.get_and_increment();
4656                        let stream_ident =
4657                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4658
4659                        match builders_or_callback {
4660                            BuildersOrCallback::Builders(graph_builders) => {
4661                                let left_lifetime = if left_top_level {
4662                                    graph_builders.cross_tick_state_lifetime(&out_location)
4663                                } else {
4664                                    graph_builders.tick_state_lifetime(&out_location)
4665                                };
4666
4667                                let right_lifetime = if right_top_level {
4668                                    graph_builders.cross_tick_state_lifetime(&out_location)
4669                                } else {
4670                                    graph_builders.tick_state_lifetime(&out_location)
4671                                };
4672
4673                                graph_builders.add_dfir_at(
4674                                    &out_location,
4675                                    if is_top_level {
4676                                        // if both inputs are root, the output is expected to have streamy semantics, so we need
4677                                        // a multiset_delta() to negate the replay behavior
4678                                        parse_quote! {
4679                                            #stream_ident = #operator::<#left_lifetime, #right_lifetime>() -> multiset_delta();
4680                                            #left_ident -> [0]#stream_ident;
4681                                            #right_ident -> [1]#stream_ident;
4682                                        }
4683                                    } else {
4684                                        parse_quote! {
4685                                            #stream_ident = #operator::<#left_lifetime, #right_lifetime>();
4686                                            #left_ident -> [0]#stream_ident;
4687                                            #right_ident -> [1]#stream_ident;
4688                                        }
4689                                    },
4690                                    Some(&stmt_id.to_string()),
4691                                );
4692                            }
4693                            BuildersOrCallback::Callback(_, node_callback) => {
4694                                node_callback(node, next_stmt_id);
4695                            }
4696                        }
4697
4698                        ident_stack.push(stream_ident);
4699                    }
4700
4701                    HydroNode::Difference { .. } | HydroNode::AntiJoin { .. } => {
4702                        let operator: syn::Ident = if matches!(node, HydroNode::Difference { .. }) {
4703                            parse_quote!(difference)
4704                        } else {
4705                            parse_quote!(anti_join)
4706                        };
4707
4708                        let (HydroNode::Difference { neg, .. } | HydroNode::AntiJoin { neg, .. }) =
4709                            node
4710                        else {
4711                            unreachable!()
4712                        };
4713
4714                        let neg_top_level = neg.metadata().location_id.is_top_level();
4715
4716                        let neg_ident = ident_stack.pop().unwrap();
4717                        let pos_ident = ident_stack.pop().unwrap();
4718
4719                        let stmt_id = next_stmt_id.get_and_increment();
4720                        let stream_ident =
4721                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4722
4723                        match builders_or_callback {
4724                            BuildersOrCallback::Builders(graph_builders) => {
4725                                let neg_lifetime = if neg_top_level {
4726                                    graph_builders.cross_tick_state_lifetime(&out_location)
4727                                } else {
4728                                    graph_builders.tick_state_lifetime(&out_location)
4729                                };
4730                                let pos_lifetime =
4731                                    graph_builders.tick_state_lifetime(&out_location);
4732
4733                                graph_builders.add_dfir_at(
4734                                    &out_location,
4735                                    parse_quote! {
4736                                        #stream_ident = #operator::<#pos_lifetime, #neg_lifetime>();
4737                                        #pos_ident -> [pos]#stream_ident;
4738                                        #neg_ident -> [neg]#stream_ident;
4739                                    },
4740                                    Some(&stmt_id.to_string()),
4741                                );
4742                            }
4743                            BuildersOrCallback::Callback(_, node_callback) => {
4744                                node_callback(node, next_stmt_id);
4745                            }
4746                        }
4747
4748                        ident_stack.push(stream_ident);
4749                    }
4750
4751                    HydroNode::JoinHalf { .. } => {
4752                        let HydroNode::JoinHalf { right, .. } = node else {
4753                            unreachable!()
4754                        };
4755
4756                        assert!(
4757                            right.metadata().collection_kind.is_bounded(),
4758                            "JoinHalf requires the right (build) side to be Bounded, got {:?}",
4759                            right.metadata().collection_kind
4760                        );
4761
4762                        let build_top_level = right.metadata().location_id.is_top_level();
4763
4764                        let build_ident = ident_stack.pop().unwrap();
4765                        let probe_ident = ident_stack.pop().unwrap();
4766
4767                        let stmt_id = next_stmt_id.get_and_increment();
4768                        let stream_ident =
4769                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4770
4771                        match builders_or_callback {
4772                            BuildersOrCallback::Builders(graph_builders) => {
4773                                let build_lifetime = if build_top_level {
4774                                    graph_builders.cross_tick_state_lifetime(&out_location)
4775                                } else {
4776                                    graph_builders.tick_state_lifetime(&out_location)
4777                                };
4778                                let probe_lifetime =
4779                                    graph_builders.tick_state_lifetime(&out_location);
4780
4781                                graph_builders.add_dfir_at(
4782                                    &out_location,
4783                                    parse_quote! {
4784                                        #stream_ident = join_multiset_half::<#build_lifetime, #probe_lifetime>();
4785                                        #probe_ident -> [probe]#stream_ident;
4786                                        #build_ident -> [build]#stream_ident;
4787                                    },
4788                                    Some(&stmt_id.to_string()),
4789                                );
4790                            }
4791                            BuildersOrCallback::Callback(_, node_callback) => {
4792                                node_callback(node, next_stmt_id);
4793                            }
4794                        }
4795
4796                        ident_stack.push(stream_ident);
4797                    }
4798
4799                    HydroNode::ResolveFutures { .. } => {
4800                        let input_ident = ident_stack.pop().unwrap();
4801
4802                        let stmt_id = next_stmt_id.get_and_increment();
4803                        let futures_ident =
4804                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4805
4806                        match builders_or_callback {
4807                            BuildersOrCallback::Builders(graph_builders) => {
4808                                graph_builders.add_dfir_at(
4809                                    &out_location,
4810                                    parse_quote! {
4811                                        #futures_ident = #input_ident -> resolve_futures();
4812                                    },
4813                                    Some(&stmt_id.to_string()),
4814                                );
4815                            }
4816                            BuildersOrCallback::Callback(_, node_callback) => {
4817                                node_callback(node, next_stmt_id);
4818                            }
4819                        }
4820
4821                        ident_stack.push(futures_ident);
4822                    }
4823
4824                    HydroNode::ResolveFuturesBlocking { .. } => {
4825                        let input_ident = ident_stack.pop().unwrap();
4826
4827                        let stmt_id = next_stmt_id.get_and_increment();
4828                        let futures_ident =
4829                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4830
4831                        match builders_or_callback {
4832                            BuildersOrCallback::Builders(graph_builders) => {
4833                                graph_builders.add_dfir_at(
4834                                    &out_location,
4835                                    parse_quote! {
4836                                        #futures_ident = #input_ident -> resolve_futures_blocking();
4837                                    },
4838                                    Some(&stmt_id.to_string()),
4839                                );
4840                            }
4841                            BuildersOrCallback::Callback(_, node_callback) => {
4842                                node_callback(node, next_stmt_id);
4843                            }
4844                        }
4845
4846                        ident_stack.push(futures_ident);
4847                    }
4848
4849                    HydroNode::ResolveFuturesOrdered { .. } => {
4850                        let input_ident = ident_stack.pop().unwrap();
4851
4852                        let stmt_id = next_stmt_id.get_and_increment();
4853                        let futures_ident =
4854                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4855
4856                        match builders_or_callback {
4857                            BuildersOrCallback::Builders(graph_builders) => {
4858                                graph_builders.add_dfir_at(
4859                                    &out_location,
4860                                    parse_quote! {
4861                                        #futures_ident = #input_ident -> resolve_futures_ordered();
4862                                    },
4863                                    Some(&stmt_id.to_string()),
4864                                );
4865                            }
4866                            BuildersOrCallback::Callback(_, node_callback) => {
4867                                node_callback(node, next_stmt_id);
4868                            }
4869                        }
4870
4871                        ident_stack.push(futures_ident);
4872                    }
4873
4874                    HydroNode::Map {
4875                        f,
4876                        input,
4877                        metadata,
4878                    } => {
4879                        // Pop input ident (pushed last by transform_children).
4880                        let input_ident = ident_stack.pop().unwrap();
4881                        let f_tokens = f.emit_tokens(&mut ident_stack);
4882
4883                        let input_ident = maybe_observe_for_mut(
4884                            f,
4885                            input_ident,
4886                            &input.metadata().location_id,
4887                            &input.metadata().collection_kind,
4888                            &metadata.op,
4889                            builders_or_callback,
4890                            next_stmt_id,
4891                        );
4892
4893                        let stmt_id = next_stmt_id.get_and_increment();
4894                        let map_ident =
4895                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4896
4897                        match builders_or_callback {
4898                            BuildersOrCallback::Builders(graph_builders) => {
4899                                graph_builders.add_dfir_at(
4900                                    &out_location,
4901                                    parse_quote! {
4902                                        #map_ident = #input_ident -> map(#f_tokens);
4903                                    },
4904                                    Some(&stmt_id.to_string()),
4905                                );
4906                            }
4907                            BuildersOrCallback::Callback(_, node_callback) => {
4908                                node_callback(node, next_stmt_id);
4909                            }
4910                        }
4911
4912                        ident_stack.push(map_ident);
4913                    }
4914
4915                    HydroNode::FlatMap { f, input, metadata } => {
4916                        let input_ident = ident_stack.pop().unwrap();
4917                        let f_tokens = f.emit_tokens(&mut ident_stack);
4918
4919                        let input_ident = maybe_observe_for_mut(
4920                            f, input_ident,
4921                            &input.metadata().location_id,
4922                            &input.metadata().collection_kind,
4923                            &metadata.op,
4924                            builders_or_callback, next_stmt_id,
4925                        );
4926
4927                        let stmt_id = next_stmt_id.get_and_increment();
4928                        let flat_map_ident =
4929                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4930
4931                        match builders_or_callback {
4932                            BuildersOrCallback::Builders(graph_builders) => {
4933                                graph_builders.add_dfir_at(
4934                                    &out_location,
4935                                    parse_quote! {
4936                                        #flat_map_ident = #input_ident -> flat_map(#f_tokens);
4937                                    },
4938                                    Some(&stmt_id.to_string()),
4939                                );
4940                            }
4941                            BuildersOrCallback::Callback(_, node_callback) => {
4942                                node_callback(node, next_stmt_id);
4943                            }
4944                        }
4945
4946                        ident_stack.push(flat_map_ident);
4947                    }
4948
4949                    HydroNode::FlatMapStreamBlocking { f, input, metadata } => {
4950                        let input_ident = ident_stack.pop().unwrap();
4951                        let f_tokens = f.emit_tokens(&mut ident_stack);
4952
4953                        let input_ident = maybe_observe_for_mut(
4954                            f, input_ident,
4955                            &input.metadata().location_id,
4956                            &input.metadata().collection_kind,
4957                            &metadata.op,
4958                            builders_or_callback, next_stmt_id,
4959                        );
4960
4961                        let stmt_id = next_stmt_id.get_and_increment();
4962                        let flat_map_stream_blocking_ident =
4963                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4964
4965                        match builders_or_callback {
4966                            BuildersOrCallback::Builders(graph_builders) => {
4967                                graph_builders.add_dfir_at(
4968                                    &out_location,
4969                                    parse_quote! {
4970                                        #flat_map_stream_blocking_ident = #input_ident -> flat_map_stream_blocking(#f_tokens);
4971                                    },
4972                                    Some(&stmt_id.to_string()),
4973                                );
4974                            }
4975                            BuildersOrCallback::Callback(_, node_callback) => {
4976                                node_callback(node, next_stmt_id);
4977                            }
4978                        }
4979
4980                        ident_stack.push(flat_map_stream_blocking_ident);
4981                    }
4982
4983                    HydroNode::Filter { f, input, metadata } => {
4984                        let input_ident = ident_stack.pop().unwrap();
4985                        let f_tokens = f.emit_tokens(&mut ident_stack);
4986
4987                        let input_ident = maybe_observe_for_mut(
4988                            f, input_ident,
4989                            &input.metadata().location_id,
4990                            &input.metadata().collection_kind,
4991                            &metadata.op,
4992                            builders_or_callback, next_stmt_id,
4993                        );
4994
4995                        let stmt_id = next_stmt_id.get_and_increment();
4996                        let filter_ident =
4997                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4998
4999                        match builders_or_callback {
5000                            BuildersOrCallback::Builders(graph_builders) => {
5001                                graph_builders.add_dfir_at(
5002                                    &out_location,
5003                                    parse_quote! {
5004                                        #filter_ident = #input_ident -> filter(#f_tokens);
5005                                    },
5006                                    Some(&stmt_id.to_string()),
5007                                );
5008                            }
5009                            BuildersOrCallback::Callback(_, node_callback) => {
5010                                node_callback(node, next_stmt_id);
5011                            }
5012                        }
5013
5014                        ident_stack.push(filter_ident);
5015                    }
5016
5017                    HydroNode::FilterMap { f, input, metadata } => {
5018                        let input_ident = ident_stack.pop().unwrap();
5019                        let f_tokens = f.emit_tokens(&mut ident_stack);
5020
5021                        let input_ident = maybe_observe_for_mut(
5022                            f, input_ident,
5023                            &input.metadata().location_id,
5024                            &input.metadata().collection_kind,
5025                            &metadata.op,
5026                            builders_or_callback, next_stmt_id,
5027                        );
5028
5029                        let stmt_id = next_stmt_id.get_and_increment();
5030                        let filter_map_ident =
5031                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5032
5033                        match builders_or_callback {
5034                            BuildersOrCallback::Builders(graph_builders) => {
5035                                graph_builders.add_dfir_at(
5036                                    &out_location,
5037                                    parse_quote! {
5038                                        #filter_map_ident = #input_ident -> filter_map(#f_tokens);
5039                                    },
5040                                    Some(&stmt_id.to_string()),
5041                                );
5042                            }
5043                            BuildersOrCallback::Callback(_, node_callback) => {
5044                                node_callback(node, next_stmt_id);
5045                            }
5046                        }
5047
5048                        ident_stack.push(filter_map_ident);
5049                    }
5050
5051                    HydroNode::Sort { .. } => {
5052                        let input_ident = ident_stack.pop().unwrap();
5053
5054                        let stmt_id = next_stmt_id.get_and_increment();
5055                        let sort_ident =
5056                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5057
5058                        match builders_or_callback {
5059                            BuildersOrCallback::Builders(graph_builders) => {
5060                                graph_builders.add_dfir_at(
5061                                    &out_location,
5062                                    parse_quote! {
5063                                        #sort_ident = #input_ident -> sort();
5064                                    },
5065                                    Some(&stmt_id.to_string()),
5066                                );
5067                            }
5068                            BuildersOrCallback::Callback(_, node_callback) => {
5069                                node_callback(node, next_stmt_id);
5070                            }
5071                        }
5072
5073                        ident_stack.push(sort_ident);
5074                    }
5075
5076                    HydroNode::DeferTick { .. } => {
5077                        let input_ident = ident_stack.pop().unwrap();
5078
5079                        let stmt_id = next_stmt_id.get_and_increment();
5080                        let defer_tick_ident =
5081                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5082
5083                        match builders_or_callback {
5084                            BuildersOrCallback::Builders(graph_builders) => {
5085                                graph_builders.add_dfir_at(
5086                                    &out_location,
5087                                    parse_quote! {
5088                                        #defer_tick_ident = #input_ident -> defer_tick_lazy();
5089                                    },
5090                                    Some(&stmt_id.to_string()),
5091                                );
5092                            }
5093                            BuildersOrCallback::Callback(_, node_callback) => {
5094                                node_callback(node, next_stmt_id);
5095                            }
5096                        }
5097
5098                        ident_stack.push(defer_tick_ident);
5099                    }
5100
5101                    HydroNode::Enumerate { input, .. } => {
5102                        let input_ident = ident_stack.pop().unwrap();
5103
5104                        let stmt_id = next_stmt_id.get_and_increment();
5105                        let enumerate_ident =
5106                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5107
5108                        match builders_or_callback {
5109                            BuildersOrCallback::Builders(graph_builders) => {
5110                                let lifetime = if input.metadata().location_id.is_top_level() {
5111                                    graph_builders.cross_tick_state_lifetime(&out_location)
5112                                } else {
5113                                    graph_builders.tick_state_lifetime(&out_location)
5114                                };
5115                                graph_builders.add_dfir_at(
5116                                    &out_location,
5117                                    parse_quote! {
5118                                        #enumerate_ident = #input_ident -> enumerate::<#lifetime>();
5119                                    },
5120                                    Some(&stmt_id.to_string()),
5121                                );
5122                            }
5123                            BuildersOrCallback::Callback(_, node_callback) => {
5124                                node_callback(node, next_stmt_id);
5125                            }
5126                        }
5127
5128                        ident_stack.push(enumerate_ident);
5129                    }
5130
5131                    HydroNode::Inspect { f, input, metadata } => {
5132                        let input_ident = ident_stack.pop().unwrap();
5133                        let f_tokens = f.emit_tokens(&mut ident_stack);
5134
5135                        let input_ident = maybe_observe_for_mut(
5136                            f, input_ident,
5137                            &input.metadata().location_id,
5138                            &input.metadata().collection_kind,
5139                            &metadata.op,
5140                            builders_or_callback, next_stmt_id,
5141                        );
5142
5143                        let stmt_id = next_stmt_id.get_and_increment();
5144                        let inspect_ident =
5145                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5146
5147                        match builders_or_callback {
5148                            BuildersOrCallback::Builders(graph_builders) => {
5149                                graph_builders.add_dfir_at(
5150                                    &out_location,
5151                                    parse_quote! {
5152                                        #inspect_ident = #input_ident -> inspect(#f_tokens);
5153                                    },
5154                                    Some(&stmt_id.to_string()),
5155                                );
5156                            }
5157                            BuildersOrCallback::Callback(_, node_callback) => {
5158                                node_callback(node, next_stmt_id);
5159                            }
5160                        }
5161
5162                        ident_stack.push(inspect_ident);
5163                    }
5164
5165                    HydroNode::Unique { input, .. } => {
5166                        let input_ident = ident_stack.pop().unwrap();
5167
5168                        let stmt_id = next_stmt_id.get_and_increment();
5169                        let unique_ident =
5170                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5171
5172                        match builders_or_callback {
5173                            BuildersOrCallback::Builders(graph_builders) => {
5174                                let lifetime = if input.metadata().location_id.is_top_level() {
5175                                    graph_builders.cross_tick_state_lifetime(&out_location)
5176                                } else {
5177                                    graph_builders.tick_state_lifetime(&out_location)
5178                                };
5179
5180                                graph_builders.add_dfir_at(
5181                                    &out_location,
5182                                    parse_quote! {
5183                                        #unique_ident = #input_ident -> unique::<#lifetime>();
5184                                    },
5185                                    Some(&stmt_id.to_string()),
5186                                );
5187                            }
5188                            BuildersOrCallback::Callback(_, node_callback) => {
5189                                node_callback(node, next_stmt_id);
5190                            }
5191                        }
5192
5193                        ident_stack.push(unique_ident);
5194                    }
5195
5196                    HydroNode::Fold { .. } | HydroNode::FoldKeyed { .. } | HydroNode::Scan { .. } | HydroNode::ScanAsyncBlocking { .. } => {
5197                        let operator: syn::Ident = if let HydroNode::Fold { input, .. } = node {
5198                            if input.metadata().location_id.is_root()
5199                                && input.metadata().collection_kind.is_bounded()
5200                            {
5201                                parse_quote!(fold_no_replay)
5202                            } else {
5203                                parse_quote!(fold)
5204                            }
5205                        } else if matches!(node, HydroNode::Scan { .. }) {
5206                            parse_quote!(scan)
5207                        } else if matches!(node, HydroNode::ScanAsyncBlocking { .. }) {
5208                            parse_quote!(scan_async_blocking)
5209                        } else if let HydroNode::FoldKeyed { input, .. } = node {
5210                            if input.metadata().location_id.is_root()
5211                                && input.metadata().collection_kind.is_bounded()
5212                            {
5213                                todo!("Fold keyed on a top-level bounded collection is not yet supported")
5214                            } else {
5215                                parse_quote!(fold_keyed)
5216                            }
5217                        } else {
5218                            unreachable!()
5219                        };
5220
5221                        let (HydroNode::Fold { input, .. }
5222                        | HydroNode::FoldKeyed { input, .. }
5223                        | HydroNode::Scan { input, .. }
5224                        | HydroNode::ScanAsyncBlocking { input, .. }) = node
5225                        else {
5226                            unreachable!()
5227                        };
5228
5229                        let input_top_level = input.metadata().location_id.is_top_level();
5230
5231                        let input_ident = ident_stack.pop().unwrap();
5232
5233                        let (HydroNode::Fold { init, acc, .. }
5234                        | HydroNode::FoldKeyed { init, acc, .. }
5235                        | HydroNode::Scan { init, acc, .. }
5236                        | HydroNode::ScanAsyncBlocking { init, acc, .. }) = &*node
5237                        else {
5238                            unreachable!()
5239                        };
5240
5241                        let acc_tokens = acc.emit_tokens(&mut ident_stack);
5242                        let init_tokens = init.emit_tokens(&mut ident_stack);
5243
5244                        let stmt_id = next_stmt_id.get_and_increment();
5245                        let fold_ident =
5246                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5247
5248                        match builders_or_callback {
5249                            BuildersOrCallback::Builders(graph_builders) => {
5250                                let lifetime = if input_top_level {
5251                                    graph_builders.cross_tick_state_lifetime(&out_location)
5252                                } else {
5253                                    graph_builders.tick_state_lifetime(&out_location)
5254                                };
5255
5256                                if matches!(node, HydroNode::Fold { .. })
5257                                    && node.metadata().location_id.is_root()
5258                                    && graph_builders.singleton_intermediates()
5259                                    && !node.metadata().collection_kind.is_bounded()
5260                                {
5261                                    let HydroNode::Fold { input, .. } = &*node else { unreachable!() };
5262                                    let hooked_input_ident = graph_builders.emit_fold_hook(
5263                                        &input.metadata().location_id,
5264                                        &input_ident,
5265                                        &input.metadata().collection_kind,
5266                                        &node.metadata().op,
5267                                    );
5268
5269                                    let (effective_input, wrapped_acc) = if let Some(ref hooked) = hooked_input_ident {
5270                                        let acc: syn::Expr = parse_quote!({
5271                                            let mut __inner = #acc_tokens;
5272                                            move |__state, __batch: Vec<_>| {
5273                                                if __batch.is_empty() {
5274                                                    return None;
5275                                                }
5276                                                for __value in __batch {
5277                                                    __inner(__state, __value);
5278                                                }
5279                                                Some(__state.clone())
5280                                            }
5281                                        });
5282                                        (hooked, acc)
5283                                    } else {
5284                                        let acc: syn::Expr = parse_quote!({
5285                                            let mut __inner = #acc_tokens;
5286                                            move |__state, __value| {
5287                                                __inner(__state, __value);
5288                                                Some(__state.clone())
5289                                            }
5290                                        });
5291                                        (&input_ident, acc)
5292                                    };
5293
5294                                    graph_builders.add_dfir_at(
5295                                        &out_location,
5296                                        parse_quote! {
5297                                            source_iter([(#init_tokens)()]) -> [0]#fold_ident;
5298                                            #effective_input -> scan::<#lifetime>(#init_tokens, #wrapped_acc) -> [1]#fold_ident;
5299                                            #fold_ident = chain();
5300                                        },
5301                                        Some(&stmt_id.to_string()),
5302                                    );
5303
5304                                    // A *scripted* fold releases exactly one element per
5305                                    // decision, producing one new version per release, so
5306                                    // its snapshot takes the ordinary (scriptable) path
5307                                    // rather than the fuzz-passthrough shortcut.
5308                                    if hooked_input_ident.is_some()
5309                                        && node.metadata().op.sim_hook_id.is_none()
5310                                    {
5311                                        fold_hooked_idents.insert(fold_ident.to_string());
5312                                    }
5313                                } else if matches!(node, HydroNode::FoldKeyed { .. })
5314                                    && node.metadata().location_id.is_root()
5315                                    && graph_builders.singleton_intermediates()
5316                                    && !node.metadata().collection_kind.is_bounded()
5317                                {
5318                                    let HydroNode::FoldKeyed { input, .. } = &*node else { unreachable!() };
5319                                    let hooked_input_ident = graph_builders.emit_fold_hook(
5320                                        &input.metadata().location_id,
5321                                        &input_ident,
5322                                        &input.metadata().collection_kind,
5323                                        &node.metadata().op,
5324                                    );
5325
5326                                    let wrapped_acc: syn::Expr = parse_quote!({
5327                                        let mut __init = #init_tokens;
5328                                        let mut __inner = #acc_tokens;
5329                                        move |__state, __kv: (_, _)| {
5330                                            // TODO(shadaj): we can avoid the clone when the entry exists
5331                                            let __state = __state
5332                                                .entry(::std::clone::Clone::clone(&__kv.0))
5333                                                .or_insert_with(|| (__init)());
5334                                            __inner(__state, __kv.1);
5335                                            Some((__kv.0, ::std::clone::Clone::clone(&*__state)))
5336                                        }
5337                                    });
5338
5339                                    if let Some(hooked_input_ident) = hooked_input_ident {
5340                                        graph_builders.add_dfir_at(
5341                                            &out_location,
5342                                            parse_quote! {
5343                                                #fold_ident = #hooked_input_ident -> flatten() -> scan::<#lifetime>(|| ::std::collections::HashMap::new(), #wrapped_acc);
5344                                            },
5345                                            Some(&stmt_id.to_string()),
5346                                        );
5347
5348                                        fold_hooked_idents.insert(fold_ident.to_string());
5349                                    } else {
5350                                        graph_builders.add_dfir_at(
5351                                            &out_location,
5352                                            parse_quote! {
5353                                                #fold_ident = #input_ident -> scan::<#lifetime>(|| ::std::collections::HashMap::new(), #wrapped_acc);
5354                                            },
5355                                            Some(&stmt_id.to_string()),
5356                                        );
5357                                    }
5358                                } else if (matches!(node, HydroNode::Fold { .. })
5359                                    || matches!(node, HydroNode::FoldKeyed { .. }))
5360                                    && !node.metadata().location_id.is_top_level()
5361                                    && graph_builders.singleton_intermediates()
5362                                {
5363                                    let input_ref = match &*node {
5364                                        HydroNode::Fold { input, .. } => input,
5365                                        HydroNode::FoldKeyed { input, .. } => input,
5366                                        _ => unreachable!(),
5367                                    };
5368                                    let hooked_input_ident = graph_builders.emit_fold_hook(
5369                                        &input_ref.metadata().location_id,
5370                                        &input_ident,
5371                                        &input_ref.metadata().collection_kind,
5372                                        &node.metadata().op,
5373                                    );
5374
5375                                    let actual_input = hooked_input_ident.as_ref().unwrap_or(&input_ident);
5376                                    graph_builders.add_dfir_at(
5377                                        &out_location,
5378                                        parse_quote! {
5379                                            #fold_ident = #actual_input -> #operator::<#lifetime>(#init_tokens, #acc_tokens);
5380                                        },
5381                                        Some(&stmt_id.to_string()),
5382                                    );
5383                                } else {
5384                                    graph_builders.add_dfir_at(
5385                                        &out_location,
5386                                        parse_quote! {
5387                                            #fold_ident = #input_ident -> #operator::<#lifetime>(#init_tokens, #acc_tokens);
5388                                        },
5389                                        Some(&stmt_id.to_string()),
5390                                    );
5391                                }
5392                            }
5393                            BuildersOrCallback::Callback(_, node_callback) => {
5394                                node_callback(node, next_stmt_id);
5395                            }
5396                        }
5397
5398                        ident_stack.push(fold_ident);
5399                    }
5400
5401                    HydroNode::Reduce { .. } | HydroNode::ReduceKeyed { .. } => {
5402                        let operator: syn::Ident = if let HydroNode::Reduce { input, .. } = node {
5403                            if input.metadata().location_id.is_root()
5404                                && input.metadata().collection_kind.is_bounded()
5405                            {
5406                                parse_quote!(reduce_no_replay)
5407                            } else {
5408                                parse_quote!(reduce)
5409                            }
5410                        } else if let HydroNode::ReduceKeyed { input, .. } = node {
5411                            if input.metadata().location_id.is_root()
5412                                && input.metadata().collection_kind.is_bounded()
5413                            {
5414                                todo!(
5415                                    "Calling keyed reduce on a top-level bounded collection is not supported"
5416                                )
5417                            } else {
5418                                parse_quote!(reduce_keyed)
5419                            }
5420                        } else {
5421                            unreachable!()
5422                        };
5423
5424                        let (HydroNode::Reduce { input, .. } | HydroNode::ReduceKeyed { input, .. }) = node
5425                        else {
5426                            unreachable!()
5427                        };
5428
5429                        let input_top_level = input.metadata().location_id.is_top_level();
5430
5431                        let input_ident = ident_stack.pop().unwrap();
5432
5433                        let (HydroNode::Reduce { f, .. } | HydroNode::ReduceKeyed { f, .. }) = &*node
5434                        else {
5435                            unreachable!()
5436                        };
5437
5438                        let f_tokens = f.emit_tokens(&mut ident_stack);
5439
5440                        let stmt_id = next_stmt_id.get_and_increment();
5441                        let reduce_ident =
5442                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5443
5444                        match builders_or_callback {
5445                            BuildersOrCallback::Builders(graph_builders) => {
5446                                let lifetime = if input_top_level {
5447                                    graph_builders.cross_tick_state_lifetime(&out_location)
5448                                } else {
5449                                    graph_builders.tick_state_lifetime(&out_location)
5450                                };
5451
5452                                if matches!(node, HydroNode::Reduce { .. })
5453                                    && node.metadata().location_id.is_root()
5454                                    && graph_builders.singleton_intermediates()
5455                                    && !node.metadata().collection_kind.is_bounded()
5456                                {
5457                                    todo!(
5458                                        "Reduce with optional intermediates is not yet supported in simulator"
5459                                    );
5460                                } else if matches!(node, HydroNode::ReduceKeyed { .. })
5461                                    && node.metadata().location_id.is_root()
5462                                    && graph_builders.singleton_intermediates()
5463                                    && !node.metadata().collection_kind.is_bounded()
5464                                {
5465                                    todo!(
5466                                        "Reduce keyed with optional intermediates is not yet supported in simulator"
5467                                    );
5468                                } else {
5469                                    graph_builders.add_dfir_at(
5470                                        &out_location,
5471                                        parse_quote! {
5472                                            #reduce_ident = #input_ident -> #operator::<#lifetime>(#f_tokens);
5473                                        },
5474                                        Some(&stmt_id.to_string()),
5475                                    );
5476                                }
5477                            }
5478                            BuildersOrCallback::Callback(_, node_callback) => {
5479                                node_callback(node, next_stmt_id);
5480                            }
5481                        }
5482
5483                        ident_stack.push(reduce_ident);
5484                    }
5485
5486                      HydroNode::ReduceKeyedWatermark {
5487                          f,
5488                          input,
5489                          watermark,
5490                          metadata,
5491                      } => {
5492
5493                          // watermark is processed second, so it's on top
5494                          let watermark_ident = ident_stack.pop().unwrap();
5495                          let watermark_location = watermark.metadata().location_id.clone();
5496                          let input_ident = ident_stack.pop().unwrap();
5497                          let f_tokens = f.emit_tokens(&mut ident_stack);
5498
5499                        let stmt_id = next_stmt_id.get_and_increment();
5500                        let chain_ident = syn::Ident::new(
5501                            &format!("reduce_keyed_watermark_chain_{}", stmt_id),
5502                            Span::call_site(),
5503                        );
5504
5505                        let fold_ident =
5506                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5507
5508                        let agg_operator: syn::Ident = if input.metadata().location_id.is_root()
5509                            && input.metadata().collection_kind.is_bounded()
5510                        {
5511                            parse_quote!(fold_no_replay)
5512                        } else {
5513                            parse_quote!(fold)
5514                        };
5515
5516                          match builders_or_callback {
5517                              BuildersOrCallback::Builders(graph_builders) => {
5518                                  // The watermark lives at its own (tick) location; if that is a
5519                                  // different loop context than where this reduce is emitted
5520                                  // (`out_location`), un-window it so the edge does not illegally
5521                                  // exit the loop.
5522                                  let watermark_ident = graph_builders.unwindow_for_consume(
5523                                      watermark_ident,
5524                                      &watermark_location,
5525                                      &out_location,
5526                                  );
5527
5528                                  let lifetime = if input.metadata().location_id.is_top_level() {
5529                                      graph_builders.cross_tick_state_lifetime(&out_location)
5530                                  } else {
5531                                      graph_builders.tick_state_lifetime(&out_location)
5532                                  };
5533
5534                                  if metadata.location_id.is_root()
5535                                      && graph_builders.singleton_intermediates()
5536                                      && !metadata.collection_kind.is_bounded()
5537                                  {
5538                                    todo!(
5539                                        "Reduce keyed watermarked on a top-level bounded collection is not yet supported"
5540                                    )
5541                                } else {
5542                                    graph_builders.add_dfir_at(
5543                                        &out_location,
5544                                        parse_quote! {
5545                                            #chain_ident = chain();
5546                                            #input_ident
5547                                                -> map(|x| (Some(x), None))
5548                                                -> [0]#chain_ident;
5549                                            #watermark_ident
5550                                                -> map(|watermark| (None, Some(watermark)))
5551                                                -> [1]#chain_ident;
5552
5553                                            #fold_ident = #chain_ident
5554                                                -> #agg_operator::<#lifetime>(|| (::std::collections::HashMap::new(), None), {
5555                                                    let __reduce_keyed_fn = #f_tokens;
5556                                                    move |(map, opt_curr_watermark), (opt_payload, opt_watermark)| {
5557                                                        if let Some((k, v)) = opt_payload {
5558                                                            if let Some(curr_watermark) = *opt_curr_watermark {
5559                                                                if k < curr_watermark {
5560                                                                    return;
5561                                                                }
5562                                                            }
5563                                                            match map.entry(k) {
5564                                                                ::std::collections::hash_map::Entry::Vacant(e) => {
5565                                                                    e.insert(v);
5566                                                                }
5567                                                                ::std::collections::hash_map::Entry::Occupied(mut e) => {
5568                                                                    __reduce_keyed_fn(e.get_mut(), v);
5569                                                                }
5570                                                            }
5571                                                        } else {
5572                                                            let watermark = opt_watermark.unwrap();
5573                                                            if let Some(curr_watermark) = *opt_curr_watermark {
5574                                                                if watermark <= curr_watermark {
5575                                                                    return;
5576                                                                }
5577                                                            }
5578                                                            map.retain(|k, _| *k >= watermark);
5579                                                            *opt_curr_watermark = Some(watermark);
5580                                                        }
5581                                                    }
5582                                                })
5583                                                -> flat_map(|(map, _curr_watermark)| map);
5584                                        },
5585                                        Some(&stmt_id.to_string()),
5586                                    );
5587                                }
5588                            }
5589                            BuildersOrCallback::Callback(_, node_callback) => {
5590                                node_callback(node, next_stmt_id);
5591                            }
5592                        }
5593
5594                        ident_stack.push(fold_ident);
5595                    }
5596
5597                    HydroNode::Network {
5598                        networking_info,
5599                        serialize,
5600                        deserialize,
5601                        instantiate_fn,
5602                        input,
5603                        ..
5604                    } => {
5605                        let input_ident = ident_stack.pop().unwrap();
5606
5607                        let stmt_id = next_stmt_id.get_and_increment();
5608                        let receiver_stream_ident =
5609                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5610
5611                        // For embedded (external) serialization, this synthesizes only the
5612                        // member-id tag conversions (if any) and passes the raw payload through.
5613                        let serialize_pipeline = serialize.pipeline();
5614                        let deserialize_pipeline = deserialize.pipeline();
5615
5616                        match builders_or_callback {
5617                            BuildersOrCallback::Builders(graph_builders) => {
5618                                let (sink_expr, source_expr) = match instantiate_fn {
5619                                    DebugInstantiate::Building => (
5620                                        syn::parse_quote!(DUMMY_SINK),
5621                                        syn::parse_quote!(DUMMY_SOURCE),
5622                                    ),
5623
5624                                    DebugInstantiate::Finalized(finalized) => {
5625                                        (finalized.sink.clone(), finalized.source.clone())
5626                                    }
5627                                };
5628
5629                                graph_builders.create_network(
5630                                    &input.metadata().location_id,
5631                                    &out_location,
5632                                    input_ident,
5633                                    &receiver_stream_ident,
5634                                    serialize_pipeline.as_ref(),
5635                                    sink_expr,
5636                                    source_expr,
5637                                    deserialize_pipeline.as_ref(),
5638                                    serialize.external_element_type(),
5639                                    stmt_id,
5640                                    networking_info,
5641                                );
5642                            }
5643                            BuildersOrCallback::Callback(_, node_callback) => {
5644                                node_callback(node, next_stmt_id);
5645                            }
5646                        }
5647
5648                        ident_stack.push(receiver_stream_ident);
5649                    }
5650
5651                    HydroNode::ExternalInput {
5652                        instantiate_fn,
5653                        deserialize_fn: deserialize_pipeline,
5654                        ..
5655                    } => {
5656                        let stmt_id = next_stmt_id.get_and_increment();
5657                        let receiver_stream_ident =
5658                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5659
5660                        match builders_or_callback {
5661                            BuildersOrCallback::Builders(graph_builders) => {
5662                                let (_, source_expr) = match instantiate_fn {
5663                                    DebugInstantiate::Building => (
5664                                        syn::parse_quote!(DUMMY_SINK),
5665                                        syn::parse_quote!(DUMMY_SOURCE),
5666                                    ),
5667
5668                                    DebugInstantiate::Finalized(finalized) => {
5669                                        (finalized.sink.clone(), finalized.source.clone())
5670                                    }
5671                                };
5672
5673                                graph_builders.create_external_source(
5674                                    &out_location,
5675                                    source_expr,
5676                                    &receiver_stream_ident,
5677                                    deserialize_pipeline.as_ref(),
5678                                    stmt_id,
5679                                );
5680                            }
5681                            BuildersOrCallback::Callback(_, node_callback) => {
5682                                node_callback(node, next_stmt_id);
5683                            }
5684                        }
5685
5686                        ident_stack.push(receiver_stream_ident);
5687                    }
5688
5689                    HydroNode::Counter {
5690                        tag,
5691                        duration,
5692                        prefix,
5693                        ..
5694                    } => {
5695                        let input_ident = ident_stack.pop().unwrap();
5696
5697                        let stmt_id = next_stmt_id.get_and_increment();
5698                        let counter_ident =
5699                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5700
5701                        match builders_or_callback {
5702                            BuildersOrCallback::Builders(graph_builders) => {
5703                                let arg = format!("{}({})", prefix, tag);
5704                                graph_builders.add_dfir_at(
5705                                    &out_location,
5706                                    parse_quote! {
5707                                        #counter_ident = #input_ident -> _counter(#arg, #duration);
5708                                    },
5709                                    Some(&stmt_id.to_string()),
5710                                );
5711                            }
5712                            BuildersOrCallback::Callback(_, node_callback) => {
5713                                node_callback(node, next_stmt_id);
5714                            }
5715                        }
5716
5717                        ident_stack.push(counter_ident);
5718                    }
5719
5720                    HydroNode::VersionedNetworkFork {
5721                        channel_id,
5722                        senders,
5723                        metadata,
5724                        ..
5725                    } => {
5726                        // sender idents are pushed in order of the 'senders' member.
5727                        let split_at = ident_stack.len() - senders.len();
5728                        let sender_idents = ident_stack.split_off(split_at);
5729
5730                        let stmt_id = next_stmt_id.get_and_increment();
5731
5732                        // All senders share the channel, so the raw element type (for embedded
5733                        // serialization) is read from the first sender.
5734                        let external_element_type =
5735                            senders.first().and_then(|(_, _, s)| s.external_element_type());
5736
5737                        match builders_or_callback {
5738                            BuildersOrCallback::Builders(graph_builders) => {
5739                                let sender_args: Vec<(LocationId, syn::Ident, Option<DebugExpr>)> =
5740                                    senders
5741                                        .iter()
5742                                        .zip(sender_idents)
5743                                        .map(|((_version, sender, serialize), ident)| {
5744                                            (
5745                                                sender.metadata().location_id.clone(),
5746                                                ident,
5747                                                serialize.pipeline(),
5748                                            )
5749                                        })
5750                                        .collect();
5751                                graph_builders.create_versioned_network_fork(
5752                                    *channel_id,
5753                                    &metadata.location_id,
5754                                    sender_args,
5755                                    external_element_type,
5756                                    stmt_id,
5757                                );
5758                            }
5759                            BuildersOrCallback::Callback(_, node_callback) => {
5760                                node_callback(node, next_stmt_id);
5761                            }
5762                        }
5763                    }
5764
5765                    HydroNode::VersionedNetwork {
5766                        fork,
5767                        deserialize,
5768                        metadata,
5769                        ..
5770                    } => {
5771                        let stmt_id = next_stmt_id.get_and_increment();
5772                        let receiver_stream_ident =
5773                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5774
5775                        // The wire element type is determined by the channel's *source* kind, which
5776                        // all senders share; read it from the shared fork's first sender.
5777                        let (channel_id, source_loc) = {
5778                            let fork_ref = fork.0.borrow();
5779                            let HydroNode::VersionedNetworkFork {
5780                                channel_id,
5781                                senders,
5782                                ..
5783                            } = &*fork_ref
5784                            else {
5785                                unreachable!("VersionedNetwork.fork must be a VersionedNetworkFork");
5786                            };
5787                            let source_loc = senders
5788                                .first()
5789                                .map(|(_v, sender, _s)| sender.metadata().location_id.clone())
5790                                .expect("a VersionedNetworkFork always has at least one sender");
5791                            (*channel_id, source_loc)
5792                        };
5793
5794                        let deserialize_pipeline = deserialize.pipeline();
5795                        let external_element_type = deserialize.external_element_type();
5796
5797                        match builders_or_callback {
5798                            BuildersOrCallback::Builders(graph_builders) => {
5799                                graph_builders.create_versioned_network(
5800                                    channel_id,
5801                                    &source_loc,
5802                                    &metadata.location_id,
5803                                    &receiver_stream_ident,
5804                                    deserialize_pipeline.as_ref(),
5805                                    external_element_type,
5806                                    stmt_id,
5807                                );
5808                            }
5809                            BuildersOrCallback::Callback(_, node_callback) => {
5810                                node_callback(node, next_stmt_id);
5811                            }
5812                        }
5813
5814                        ident_stack.push(receiver_stream_ident);
5815                    }
5816                }
5817            },
5818            seen_tees,
5819            false,
5820        );
5821
5822        let ret = ident_stack
5823            .pop()
5824            .expect("ident_stack should have exactly one element after traversal");
5825        assert!(
5826            ident_stack.is_empty(),
5827            "ident_stack should be empty after popping the final ident, but has {} remaining element(s). \
5828             This indicates a bug in the code gen: some node pushed idents that were never consumed.",
5829            ident_stack.len()
5830        );
5831        ret
5832    }
5833
5834    pub fn visit_debug_expr(&mut self, mut transform: impl FnMut(&mut DebugExpr)) {
5835        match self {
5836            HydroNode::Placeholder => {
5837                panic!()
5838            }
5839            HydroNode::Cast { .. }
5840            | HydroNode::ObserveNonDet { .. }
5841            | HydroNode::UnboundSingleton { .. }
5842            | HydroNode::AssertIsConsistent { .. } => {}
5843            HydroNode::Source { source, .. } => match source {
5844                HydroSource::Stream(expr) | HydroSource::Iter(expr) => transform(expr),
5845                HydroSource::ExternalNetwork()
5846                | HydroSource::Spin()
5847                | HydroSource::ClusterMembers(_, _)
5848                | HydroSource::Embedded(_)
5849                | HydroSource::EmbeddedSingleton(_) => {} // TODO: what goes here?
5850            },
5851            HydroNode::SingletonSource { value, .. } => {
5852                transform(value);
5853            }
5854            HydroNode::CycleSource { .. }
5855            | HydroNode::Tee { .. }
5856            | HydroNode::Reference { .. }
5857            | HydroNode::YieldConcat { .. }
5858            | HydroNode::BeginAtomic { .. }
5859            | HydroNode::EndAtomic { .. }
5860            | HydroNode::Batch { .. }
5861            | HydroNode::Chain { .. }
5862            | HydroNode::MergeOrdered { .. }
5863            | HydroNode::ChainFirst { .. }
5864            | HydroNode::CrossProduct { .. }
5865            | HydroNode::CrossSingleton { .. }
5866            | HydroNode::ResolveFutures { .. }
5867            | HydroNode::ResolveFuturesBlocking { .. }
5868            | HydroNode::ResolveFuturesOrdered { .. }
5869            | HydroNode::Join { .. }
5870            | HydroNode::JoinHalf { .. }
5871            | HydroNode::Difference { .. }
5872            | HydroNode::AntiJoin { .. }
5873            | HydroNode::DeferTick { .. }
5874            | HydroNode::Enumerate { .. }
5875            | HydroNode::Unique { .. }
5876            | HydroNode::Sort { .. }
5877            | HydroNode::PartitionSide { .. }
5878            | HydroNode::VersionedNetworkFork { .. }
5879            | HydroNode::VersionedNetwork { .. } => {}
5880            HydroNode::Map { f, .. }
5881            | HydroNode::FlatMap { f, .. }
5882            | HydroNode::FlatMapStreamBlocking { f, .. }
5883            | HydroNode::Filter { f, .. }
5884            | HydroNode::FilterMap { f, .. }
5885            | HydroNode::Inspect { f, .. }
5886            | HydroNode::PartitionShared { f, .. }
5887            | HydroNode::Reduce { f, .. }
5888            | HydroNode::ReduceKeyed { f, .. }
5889            | HydroNode::ReduceKeyedWatermark { f, .. } => {
5890                transform(&mut f.expr);
5891            }
5892            HydroNode::Fold { init, acc, .. }
5893            | HydroNode::Scan { init, acc, .. }
5894            | HydroNode::ScanAsyncBlocking { init, acc, .. }
5895            | HydroNode::FoldKeyed { init, acc, .. } => {
5896                transform(&mut init.expr);
5897                transform(&mut acc.expr);
5898            }
5899            HydroNode::Network {
5900                serialize,
5901                deserialize,
5902                ..
5903            } => {
5904                if let NetworkSend::Custom {
5905                    serialize_fn: Some(serialize_fn),
5906                } = serialize
5907                {
5908                    transform(serialize_fn);
5909                }
5910                if let NetworkRecv::Custom {
5911                    deserialize_fn: Some(deserialize_fn),
5912                } = deserialize
5913                {
5914                    transform(deserialize_fn);
5915                }
5916            }
5917            HydroNode::ExternalInput { deserialize_fn, .. } => {
5918                if let Some(deserialize_fn) = deserialize_fn {
5919                    transform(deserialize_fn);
5920                }
5921            }
5922            HydroNode::Counter { duration, .. } => {
5923                transform(duration);
5924            }
5925        }
5926    }
5927
5928    pub fn op_metadata(&self) -> &HydroIrOpMetadata {
5929        &self.metadata().op
5930    }
5931
5932    pub fn metadata(&self) -> &HydroIrMetadata {
5933        match self {
5934            HydroNode::Placeholder => {
5935                panic!()
5936            }
5937            HydroNode::VersionedNetworkFork { metadata, .. }
5938            | HydroNode::VersionedNetwork { metadata, .. } => metadata,
5939            HydroNode::Cast { metadata, .. }
5940            | HydroNode::ObserveNonDet { metadata, .. }
5941            | HydroNode::AssertIsConsistent { metadata, .. }
5942            | HydroNode::UnboundSingleton { metadata, .. }
5943            | HydroNode::Source { metadata, .. }
5944            | HydroNode::SingletonSource { metadata, .. }
5945            | HydroNode::CycleSource { metadata, .. }
5946            | HydroNode::Tee { metadata, .. }
5947            | HydroNode::Reference { metadata, .. }
5948            | HydroNode::PartitionSide { metadata, .. }
5949            | HydroNode::PartitionShared { metadata, .. }
5950            | HydroNode::YieldConcat { metadata, .. }
5951            | HydroNode::BeginAtomic { metadata, .. }
5952            | HydroNode::EndAtomic { metadata, .. }
5953            | HydroNode::Batch { metadata, .. }
5954            | HydroNode::Chain { metadata, .. }
5955            | HydroNode::MergeOrdered { metadata, .. }
5956            | HydroNode::ChainFirst { metadata, .. }
5957            | HydroNode::CrossProduct { metadata, .. }
5958            | HydroNode::CrossSingleton { metadata, .. }
5959            | HydroNode::Join { metadata, .. }
5960            | HydroNode::JoinHalf { metadata, .. }
5961            | HydroNode::Difference { metadata, .. }
5962            | HydroNode::AntiJoin { metadata, .. }
5963            | HydroNode::ResolveFutures { metadata, .. }
5964            | HydroNode::ResolveFuturesBlocking { metadata, .. }
5965            | HydroNode::ResolveFuturesOrdered { metadata, .. }
5966            | HydroNode::Map { metadata, .. }
5967            | HydroNode::FlatMap { metadata, .. }
5968            | HydroNode::FlatMapStreamBlocking { metadata, .. }
5969            | HydroNode::Filter { metadata, .. }
5970            | HydroNode::FilterMap { metadata, .. }
5971            | HydroNode::DeferTick { metadata, .. }
5972            | HydroNode::Enumerate { metadata, .. }
5973            | HydroNode::Inspect { metadata, .. }
5974            | HydroNode::Unique { metadata, .. }
5975            | HydroNode::Sort { metadata, .. }
5976            | HydroNode::Scan { metadata, .. }
5977            | HydroNode::ScanAsyncBlocking { metadata, .. }
5978            | HydroNode::Fold { metadata, .. }
5979            | HydroNode::FoldKeyed { metadata, .. }
5980            | HydroNode::Reduce { metadata, .. }
5981            | HydroNode::ReduceKeyed { metadata, .. }
5982            | HydroNode::ReduceKeyedWatermark { metadata, .. }
5983            | HydroNode::ExternalInput { metadata, .. }
5984            | HydroNode::Network { metadata, .. }
5985            | HydroNode::Counter { metadata, .. } => metadata,
5986        }
5987    }
5988
5989    pub fn op_metadata_mut(&mut self) -> &mut HydroIrOpMetadata {
5990        &mut self.metadata_mut().op
5991    }
5992
5993    pub fn metadata_mut(&mut self) -> &mut HydroIrMetadata {
5994        match self {
5995            HydroNode::Placeholder => {
5996                panic!()
5997            }
5998            HydroNode::VersionedNetworkFork { metadata, .. }
5999            | HydroNode::VersionedNetwork { metadata, .. } => metadata,
6000            HydroNode::Cast { metadata, .. }
6001            | HydroNode::ObserveNonDet { metadata, .. }
6002            | HydroNode::AssertIsConsistent { metadata, .. }
6003            | HydroNode::UnboundSingleton { metadata, .. }
6004            | HydroNode::Source { metadata, .. }
6005            | HydroNode::SingletonSource { metadata, .. }
6006            | HydroNode::CycleSource { metadata, .. }
6007            | HydroNode::Tee { metadata, .. }
6008            | HydroNode::Reference { metadata, .. }
6009            | HydroNode::PartitionSide { metadata, .. }
6010            | HydroNode::PartitionShared { metadata, .. }
6011            | HydroNode::YieldConcat { metadata, .. }
6012            | HydroNode::BeginAtomic { metadata, .. }
6013            | HydroNode::EndAtomic { metadata, .. }
6014            | HydroNode::Batch { metadata, .. }
6015            | HydroNode::Chain { metadata, .. }
6016            | HydroNode::MergeOrdered { metadata, .. }
6017            | HydroNode::ChainFirst { metadata, .. }
6018            | HydroNode::CrossProduct { metadata, .. }
6019            | HydroNode::CrossSingleton { metadata, .. }
6020            | HydroNode::Join { metadata, .. }
6021            | HydroNode::JoinHalf { metadata, .. }
6022            | HydroNode::Difference { metadata, .. }
6023            | HydroNode::AntiJoin { metadata, .. }
6024            | HydroNode::ResolveFutures { metadata, .. }
6025            | HydroNode::ResolveFuturesBlocking { metadata, .. }
6026            | HydroNode::ResolveFuturesOrdered { metadata, .. }
6027            | HydroNode::Map { metadata, .. }
6028            | HydroNode::FlatMap { metadata, .. }
6029            | HydroNode::FlatMapStreamBlocking { metadata, .. }
6030            | HydroNode::Filter { metadata, .. }
6031            | HydroNode::FilterMap { metadata, .. }
6032            | HydroNode::DeferTick { metadata, .. }
6033            | HydroNode::Enumerate { metadata, .. }
6034            | HydroNode::Inspect { metadata, .. }
6035            | HydroNode::Unique { metadata, .. }
6036            | HydroNode::Sort { metadata, .. }
6037            | HydroNode::Scan { metadata, .. }
6038            | HydroNode::ScanAsyncBlocking { metadata, .. }
6039            | HydroNode::Fold { metadata, .. }
6040            | HydroNode::FoldKeyed { metadata, .. }
6041            | HydroNode::Reduce { metadata, .. }
6042            | HydroNode::ReduceKeyed { metadata, .. }
6043            | HydroNode::ReduceKeyedWatermark { metadata, .. }
6044            | HydroNode::ExternalInput { metadata, .. }
6045            | HydroNode::Network { metadata, .. }
6046            | HydroNode::Counter { metadata, .. } => metadata,
6047        }
6048    }
6049
6050    pub fn input(&self) -> Vec<&HydroNode> {
6051        match self {
6052            HydroNode::Placeholder => {
6053                panic!()
6054            }
6055            HydroNode::Source { .. }
6056            | HydroNode::SingletonSource { .. }
6057            | HydroNode::ExternalInput { .. }
6058            | HydroNode::CycleSource { .. }
6059            | HydroNode::Tee { .. }
6060            | HydroNode::Reference { .. }
6061            | HydroNode::PartitionSide { .. }
6062            | HydroNode::VersionedNetwork { .. } => {
6063                // Tee/PartitionSide/VersionedNetwork find their input in separate special ways
6064                vec![]
6065            }
6066            HydroNode::Cast { inner, .. }
6067            | HydroNode::ObserveNonDet { inner, .. }
6068            | HydroNode::YieldConcat { inner, .. }
6069            | HydroNode::BeginAtomic { inner, .. }
6070            | HydroNode::EndAtomic { inner, .. }
6071            | HydroNode::Batch { inner, .. }
6072            | HydroNode::UnboundSingleton { inner, .. }
6073            | HydroNode::AssertIsConsistent { inner, .. } => {
6074                vec![inner]
6075            }
6076            HydroNode::Chain { first, second, .. }
6077            | HydroNode::MergeOrdered { first, second, .. }
6078            | HydroNode::ChainFirst { first, second, .. } => {
6079                vec![first, second]
6080            }
6081            HydroNode::CrossProduct { left, right, .. }
6082            | HydroNode::CrossSingleton { left, right, .. }
6083            | HydroNode::Join { left, right, .. }
6084            | HydroNode::JoinHalf { left, right, .. } => {
6085                vec![left, right]
6086            }
6087            HydroNode::Difference { pos, neg, .. } | HydroNode::AntiJoin { pos, neg, .. } => {
6088                vec![pos, neg]
6089            }
6090            HydroNode::Counter { input, .. }
6091            | HydroNode::DeferTick { input, .. }
6092            | HydroNode::Enumerate { input, .. }
6093            | HydroNode::Filter { input, .. }
6094            | HydroNode::FilterMap { input, .. }
6095            | HydroNode::FlatMap { input, .. }
6096            | HydroNode::FlatMapStreamBlocking { input, .. }
6097            | HydroNode::Fold { input, .. }
6098            | HydroNode::FoldKeyed { input, .. }
6099            | HydroNode::Inspect { input, .. }
6100            | HydroNode::Map { input, .. }
6101            | HydroNode::Network { input, .. }
6102            | HydroNode::PartitionShared { input, .. }
6103            | HydroNode::Reduce { input, .. }
6104            | HydroNode::ReduceKeyed { input, .. }
6105            | HydroNode::ResolveFutures { input, .. }
6106            | HydroNode::ResolveFuturesBlocking { input, .. }
6107            | HydroNode::ResolveFuturesOrdered { input, .. }
6108            | HydroNode::Scan { input, .. }
6109            | HydroNode::ScanAsyncBlocking { input, .. }
6110            | HydroNode::Sort { input, .. }
6111            | HydroNode::Unique { input, .. } => {
6112                vec![input]
6113            }
6114            HydroNode::ReduceKeyedWatermark {
6115                input, watermark, ..
6116            } => {
6117                vec![input, watermark]
6118            }
6119            HydroNode::VersionedNetworkFork { senders, .. } => senders
6120                .iter()
6121                .map(|(_version, sender, _serialize)| sender.as_ref())
6122                .collect(),
6123        }
6124    }
6125
6126    pub fn input_metadata(&self) -> Vec<&HydroIrMetadata> {
6127        self.input()
6128            .iter()
6129            .map(|input_node| input_node.metadata())
6130            .collect()
6131    }
6132
6133    /// Returns `true` if this node is a Tee or Partition whose inner Rc
6134    /// has other live references, meaning the upstream is already driven
6135    /// by another consumer and does not need a Null sink.
6136    pub fn is_shared_with_others(&self) -> bool {
6137        match self {
6138            HydroNode::Tee { inner, .. } | HydroNode::PartitionSide { inner, .. } => {
6139                Rc::strong_count(&inner.0) > 1
6140            }
6141            // A zero-output reference node is valid in DFIR (it drains itself at
6142            // end of tick), so it doesn't need to be driven by another consumer.
6143            HydroNode::Reference { .. } => false,
6144            _ => false,
6145        }
6146    }
6147
6148    pub fn print_root(&self) -> String {
6149        match self {
6150            HydroNode::Placeholder => {
6151                panic!()
6152            }
6153            HydroNode::Cast { .. } => "Cast()".to_owned(),
6154            HydroNode::UnboundSingleton { .. } => "UnboundSingleton()".to_owned(),
6155            HydroNode::ObserveNonDet { .. } => "ObserveNonDet()".to_owned(),
6156            HydroNode::AssertIsConsistent { .. } => "AssertIsConsistent()".to_owned(),
6157            HydroNode::Source { source, .. } => format!("Source({:?})", source),
6158            HydroNode::SingletonSource {
6159                value,
6160                first_tick_only,
6161                ..
6162            } => format!(
6163                "SingletonSource({:?}, first_tick_only={})",
6164                value, first_tick_only
6165            ),
6166            HydroNode::CycleSource { cycle_id, .. } => format!("CycleSource({})", cycle_id),
6167            HydroNode::Tee { inner, .. } => {
6168                format!("Tee({})", inner.0.borrow().print_root())
6169            }
6170            HydroNode::Reference { inner, kind, .. } => {
6171                format!("Reference({:?}, {})", kind, inner.0.borrow().print_root())
6172            }
6173            HydroNode::PartitionSide { inner, is_true, .. } => {
6174                format!(
6175                    "PartitionSide(is_true={}, {})",
6176                    is_true,
6177                    inner.0.borrow().print_root(),
6178                )
6179            }
6180            HydroNode::PartitionShared { f, .. } => format!("PartitionShared({:?})", f),
6181            HydroNode::YieldConcat { .. } => "YieldConcat()".to_owned(),
6182            HydroNode::BeginAtomic { .. } => "BeginAtomic()".to_owned(),
6183            HydroNode::EndAtomic { .. } => "EndAtomic()".to_owned(),
6184            HydroNode::Batch { .. } => "Batch()".to_owned(),
6185            HydroNode::Chain { first, second, .. } => {
6186                format!("Chain({}, {})", first.print_root(), second.print_root())
6187            }
6188            HydroNode::MergeOrdered { first, second, .. } => {
6189                format!(
6190                    "MergeOrdered({}, {})",
6191                    first.print_root(),
6192                    second.print_root()
6193                )
6194            }
6195            HydroNode::ChainFirst { first, second, .. } => {
6196                format!(
6197                    "ChainFirst({}, {})",
6198                    first.print_root(),
6199                    second.print_root()
6200                )
6201            }
6202            HydroNode::CrossProduct { left, right, .. } => {
6203                format!(
6204                    "CrossProduct({}, {})",
6205                    left.print_root(),
6206                    right.print_root()
6207                )
6208            }
6209            HydroNode::CrossSingleton { left, right, .. } => {
6210                format!(
6211                    "CrossSingleton({}, {})",
6212                    left.print_root(),
6213                    right.print_root()
6214                )
6215            }
6216            HydroNode::Join { left, right, .. } => {
6217                format!("Join({}, {})", left.print_root(), right.print_root())
6218            }
6219            HydroNode::JoinHalf { left, right, .. } => {
6220                format!("JoinHalf({}, {})", left.print_root(), right.print_root())
6221            }
6222            HydroNode::Difference { pos, neg, .. } => {
6223                format!("Difference({}, {})", pos.print_root(), neg.print_root())
6224            }
6225            HydroNode::AntiJoin { pos, neg, .. } => {
6226                format!("AntiJoin({}, {})", pos.print_root(), neg.print_root())
6227            }
6228            HydroNode::ResolveFutures { .. } => "ResolveFutures()".to_owned(),
6229            HydroNode::ResolveFuturesBlocking { .. } => "ResolveFuturesBlocking()".to_owned(),
6230            HydroNode::ResolveFuturesOrdered { .. } => "ResolveFuturesOrdered()".to_owned(),
6231            HydroNode::Map { f, .. } => format!("Map({:?})", f),
6232            HydroNode::FlatMap { f, .. } => format!("FlatMap({:?})", f),
6233            HydroNode::FlatMapStreamBlocking { f, .. } => format!("FlatMapStreamBlocking({:?})", f),
6234            HydroNode::Filter { f, .. } => format!("Filter({:?})", f),
6235            HydroNode::FilterMap { f, .. } => format!("FilterMap({:?})", f),
6236            HydroNode::DeferTick { .. } => "DeferTick()".to_owned(),
6237            HydroNode::Enumerate { .. } => "Enumerate()".to_owned(),
6238            HydroNode::Inspect { f, .. } => format!("Inspect({:?})", f),
6239            HydroNode::Unique { .. } => "Unique()".to_owned(),
6240            HydroNode::Sort { .. } => "Sort()".to_owned(),
6241            HydroNode::Fold { init, acc, .. } => format!("Fold({:?}, {:?})", init, acc),
6242            HydroNode::Scan { init, acc, .. } => format!("Scan({:?}, {:?})", init, acc),
6243            HydroNode::ScanAsyncBlocking { init, acc, .. } => {
6244                format!("ScanAsyncBlocking({:?}, {:?})", init, acc)
6245            }
6246            HydroNode::FoldKeyed { init, acc, .. } => format!("FoldKeyed({:?}, {:?})", init, acc),
6247            HydroNode::Reduce { f, .. } => format!("Reduce({:?})", f),
6248            HydroNode::ReduceKeyed { f, .. } => format!("ReduceKeyed({:?})", f),
6249            HydroNode::ReduceKeyedWatermark { f, .. } => format!("ReduceKeyedWatermark({:?})", f),
6250            HydroNode::Network { .. } => "Network()".to_owned(),
6251            HydroNode::ExternalInput { .. } => "ExternalInput()".to_owned(),
6252            HydroNode::Counter { tag, duration, .. } => {
6253                format!("Counter({:?}, {:?})", tag, duration)
6254            }
6255            HydroNode::VersionedNetworkFork {
6256                channel_name,
6257                senders,
6258                ..
6259            } => {
6260                let versions: Vec<u32> = senders.iter().map(|(v, _, _)| *v).collect();
6261                format!(
6262                    "VersionedNetworkFork({}, senders={:?})",
6263                    channel_name, versions
6264                )
6265            }
6266            HydroNode::VersionedNetwork { version, .. } => {
6267                format!("VersionedNetwork(v{})", version)
6268            }
6269        }
6270    }
6271}
6272
6273#[cfg(feature = "build")]
6274#[expect(clippy::too_many_arguments, reason = "networking codegen")]
6275fn instantiate_network<'a, D>(
6276    env: &mut D::InstantiateEnv,
6277    from_location: &LocationId,
6278    to_location: &LocationId,
6279    processes: &SparseSecondaryMap<LocationKey, D::Process>,
6280    clusters: &SparseSecondaryMap<LocationKey, D::Cluster>,
6281    name: Option<&str>,
6282    networking_info: &crate::networking::NetworkingInfo,
6283    external_types: Option<(&syn::Type, &syn::Type)>,
6284) -> (syn::Expr, syn::Expr, Box<dyn FnOnce()>)
6285where
6286    D: Deploy<'a>,
6287{
6288    if external_types.is_some() && !D::SUPPORTS_EXTERNAL_SERIALIZATION {
6289        panic!(
6290            "`.embedded()` serialization leaves serialization to code outside of Hydro and is \
6291             only supported by the embedded deployment backend. Use `.bincode()` (or another \
6292             supported serialization backend) for this deployment target instead."
6293        );
6294    }
6295
6296    let ((sink, source), connect_fn) = match (from_location, to_location) {
6297        (&LocationId::Process(from), &LocationId::Process(to)) => {
6298            let from_node = processes
6299                .get(from)
6300                .unwrap_or_else(|| {
6301                    panic!("A process used in the graph was not instantiated: {}", from)
6302                })
6303                .clone();
6304            let to_node = processes
6305                .get(to)
6306                .unwrap_or_else(|| {
6307                    panic!("A process used in the graph was not instantiated: {}", to)
6308                })
6309                .clone();
6310
6311            let sink_port = from_node.next_port();
6312            let source_port = to_node.next_port();
6313
6314            (
6315                D::o2o_sink_source(
6316                    env,
6317                    &from_node,
6318                    &sink_port,
6319                    &to_node,
6320                    &source_port,
6321                    name,
6322                    networking_info,
6323                    external_types,
6324                ),
6325                D::o2o_connect(&from_node, &sink_port, &to_node, &source_port),
6326            )
6327        }
6328        (&LocationId::Process(from), &LocationId::Cluster(to)) => {
6329            let from_node = processes
6330                .get(from)
6331                .unwrap_or_else(|| {
6332                    panic!("A process used in the graph was not instantiated: {}", from)
6333                })
6334                .clone();
6335            let to_node = clusters
6336                .get(to)
6337                .unwrap_or_else(|| {
6338                    panic!("A cluster used in the graph was not instantiated: {}", to)
6339                })
6340                .clone();
6341
6342            let sink_port = from_node.next_port();
6343            let source_port = to_node.next_port();
6344
6345            (
6346                D::o2m_sink_source(
6347                    env,
6348                    &from_node,
6349                    &sink_port,
6350                    &to_node,
6351                    &source_port,
6352                    name,
6353                    networking_info,
6354                    external_types,
6355                ),
6356                D::o2m_connect(&from_node, &sink_port, &to_node, &source_port),
6357            )
6358        }
6359        (&LocationId::Cluster(from), &LocationId::Process(to)) => {
6360            let from_node = clusters
6361                .get(from)
6362                .unwrap_or_else(|| {
6363                    panic!("A cluster used in the graph was not instantiated: {}", from)
6364                })
6365                .clone();
6366            let to_node = processes
6367                .get(to)
6368                .unwrap_or_else(|| {
6369                    panic!("A process used in the graph was not instantiated: {}", to)
6370                })
6371                .clone();
6372
6373            let sink_port = from_node.next_port();
6374            let source_port = to_node.next_port();
6375
6376            (
6377                D::m2o_sink_source(
6378                    env,
6379                    &from_node,
6380                    &sink_port,
6381                    &to_node,
6382                    &source_port,
6383                    name,
6384                    networking_info,
6385                    external_types,
6386                ),
6387                D::m2o_connect(&from_node, &sink_port, &to_node, &source_port),
6388            )
6389        }
6390        (&LocationId::Cluster(from), &LocationId::Cluster(to)) => {
6391            let from_node = clusters
6392                .get(from)
6393                .unwrap_or_else(|| {
6394                    panic!("A cluster used in the graph was not instantiated: {}", from)
6395                })
6396                .clone();
6397            let to_node = clusters
6398                .get(to)
6399                .unwrap_or_else(|| {
6400                    panic!("A cluster used in the graph was not instantiated: {}", to)
6401                })
6402                .clone();
6403
6404            let sink_port = from_node.next_port();
6405            let source_port = to_node.next_port();
6406
6407            (
6408                D::m2m_sink_source(
6409                    env,
6410                    &from_node,
6411                    &sink_port,
6412                    &to_node,
6413                    &source_port,
6414                    name,
6415                    networking_info,
6416                    external_types,
6417                ),
6418                D::m2m_connect(&from_node, &sink_port, &to_node, &source_port),
6419            )
6420        }
6421        (LocationId::Tick { .. }, _) => panic!(),
6422        (_, LocationId::Tick { .. }) => panic!(),
6423        (LocationId::Atomic(_), _) => panic!(),
6424        (_, LocationId::Atomic(_)) => panic!(),
6425    };
6426    (sink, source, connect_fn)
6427}
6428
6429#[cfg(test)]
6430mod serde_test;
6431
6432#[cfg(test)]
6433mod test {
6434    use std::mem::size_of;
6435
6436    use stageleft::{QuotedWithContext, q};
6437
6438    use super::*;
6439
6440    #[test]
6441    #[cfg_attr(
6442        not(feature = "build"),
6443        ignore = "expects inclusion of feature-gated fields"
6444    )]
6445    fn hydro_node_size() {
6446        assert_eq!(size_of::<HydroNode>(), 280);
6447    }
6448
6449    #[test]
6450    #[cfg_attr(
6451        not(feature = "build"),
6452        ignore = "expects inclusion of feature-gated fields"
6453    )]
6454    fn hydro_root_size() {
6455        assert_eq!(size_of::<HydroRoot>(), 152);
6456    }
6457
6458    #[test]
6459    fn test_simplify_q_macro_basic() {
6460        // Test basic non-q! expression
6461        let simple_expr: syn::Expr = syn::parse_str("x + y").unwrap();
6462        let result = simplify_q_macro(simple_expr.clone());
6463        assert_eq!(result, simple_expr);
6464    }
6465
6466    #[test]
6467    fn test_simplify_q_macro_actual_stageleft_call() {
6468        // Test a simplified version of what a real stageleft call might look like
6469        let stageleft_call = q!(|x: usize| x + 1).splice_fn1_ctx(&());
6470        let result = simplify_q_macro(stageleft_call);
6471        // This should be processed by our visitor and simplified to q!(...)
6472        // since we detect the stageleft::runtime_support::fn_* pattern
6473        hydro_build_utils::assert_snapshot!(result.to_token_stream().to_string());
6474    }
6475
6476    #[test]
6477    fn test_closure_no_pipe_at_start() {
6478        // Test a closure that does not start with a pipe
6479        let stageleft_call = q!({
6480            let foo = 123;
6481            move |b: usize| b + foo
6482        })
6483        .splice_fn1_ctx(&());
6484        let result = simplify_q_macro(stageleft_call);
6485        hydro_build_utils::assert_snapshot!(result.to_token_stream().to_string());
6486    }
6487}