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