Skip to main content

hydro_lang/compile/ir/
mod.rs

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