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, Debug)]
2930pub enum StreamRetry {
2931    AtLeastOnce,
2932    ExactlyOnce,
2933}
2934
2935#[derive(serde::Serialize, Clone, PartialEq, Eq, Debug)]
2936pub enum KeyedSingletonBoundKind {
2937    Unbounded,
2938    MonotonicKeys,
2939    MonotonicValue,
2940    BoundedValue,
2941    Bounded,
2942}
2943
2944#[derive(serde::Serialize, Clone, PartialEq, Eq, Debug)]
2945pub enum SingletonBoundKind {
2946    Unbounded,
2947    Monotonic,
2948    Bounded,
2949}
2950
2951#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize)]
2952pub enum CollectionKind {
2953    Stream {
2954        bound: BoundKind,
2955        order: StreamOrder,
2956        retry: StreamRetry,
2957        element_type: DebugType,
2958    },
2959    Singleton {
2960        bound: SingletonBoundKind,
2961        element_type: DebugType,
2962    },
2963    Optional {
2964        bound: OptionalBoundKind,
2965        element_type: DebugType,
2966    },
2967    KeyedStream {
2968        bound: BoundKind,
2969        value_order: StreamOrder,
2970        value_retry: StreamRetry,
2971        key_type: DebugType,
2972        value_type: DebugType,
2973    },
2974    KeyedSingleton {
2975        bound: KeyedSingletonBoundKind,
2976        key_type: DebugType,
2977        value_type: DebugType,
2978    },
2979}
2980
2981impl CollectionKind {
2982    pub fn is_bounded(&self) -> bool {
2983        matches!(
2984            self,
2985            CollectionKind::Stream {
2986                bound: BoundKind::Bounded,
2987                ..
2988            } | CollectionKind::Singleton {
2989                bound: SingletonBoundKind::Bounded,
2990                ..
2991            } | CollectionKind::Optional {
2992                bound: OptionalBoundKind::Bounded,
2993                ..
2994            } | CollectionKind::KeyedStream {
2995                bound: BoundKind::Bounded,
2996                ..
2997            } | CollectionKind::KeyedSingleton {
2998                bound: KeyedSingletonBoundKind::Bounded,
2999                ..
3000            }
3001        )
3002    }
3003
3004    /// Returns whether this collection kind is already "strict" (`TotalOrder` + `ExactlyOnce`),
3005    /// meaning no non-determinism needs to be observed for mut closures.
3006    pub fn is_strict(&self) -> bool {
3007        match self {
3008            CollectionKind::Stream { order, retry, .. } => {
3009                *order == StreamOrder::TotalOrder && *retry == StreamRetry::ExactlyOnce
3010            }
3011            CollectionKind::KeyedStream {
3012                value_order,
3013                value_retry,
3014                ..
3015            } => {
3016                *value_order == StreamOrder::TotalOrder && *value_retry == StreamRetry::ExactlyOnce
3017            }
3018            // Singletons/Optionals/KeyedSingletons do not have observable
3019            // non-determinism other than snapshots / batching
3020            CollectionKind::Singleton { .. }
3021            | CollectionKind::Optional { .. }
3022            | CollectionKind::KeyedSingleton { .. } => true,
3023        }
3024    }
3025
3026    /// Returns whether this collection kind, when materialized at a **root** (non-tick,
3027    /// non-atomic) location, is represented as a "tombstoned update feed": a DFIR pipe whose
3028    /// elements are explicit update records that can express *removal*, rather than plain
3029    /// values.
3030    ///
3031    /// - [`CollectionKind::Optional`] with [`OptionalBoundKind::Unbounded`]: the pipe carries
3032    ///   `Option<T>` updates, where `None` is a tombstone recording that the optional became
3033    ///   null. (Bounded and `InitNone` optionals never become null after having a value, so
3034    ///   they are represented as plain `T` updates.)
3035    /// - [`CollectionKind::KeyedSingleton`] with [`KeyedSingletonBoundKind::Unbounded`]: the
3036    ///   pipe carries `(K, Option<V>)` updates, where `(k, Some(v))` upserts an entry and
3037    ///   `(k, None)` is a tombstone recording that key `k` was removed (e.g. garbage-collected
3038    ///   by `KeyedStream::reduce_watermark` when the watermark advances). The other keyed
3039    ///   bound kinds never remove keys (or are bounded), so they keep plain `(K, V)` entries.
3040    ///
3041    /// This representation exists because a plain update feed cannot distinguish "no update"
3042    /// from "became absent" (#3181). It applies **only** at root locations: inside a tick (or
3043    /// atomic region fused into a tick's loop), presence is re-derived every firing, so
3044    /// absence in a firing is already meaningful and plain values are used.
3045    ///
3046    /// Producers of tombstoned feeds must emit an explicit tombstone for every removal (see
3047    /// the delta-emitting `scan` in the [`HydroNode::ReduceKeyedWatermark`] codegen);
3048    /// consumers hold the latest state across firings by applying updates (see
3049    /// [`DfirBuilder::batch`]). Generated code for keyed feeds requires `K: Clone` (and
3050    /// `V: Clone`), which the pre-existing whole-map re-emission already required.
3051    pub fn root_repr_is_tombstoned_update_feed(&self) -> bool {
3052        matches!(
3053            self,
3054            CollectionKind::Optional {
3055                bound: OptionalBoundKind::Unbounded,
3056                ..
3057            } | CollectionKind::KeyedSingleton {
3058                bound: KeyedSingletonBoundKind::Unbounded,
3059                ..
3060            }
3061        )
3062    }
3063
3064    /// Creates a "strict" version of this kind with `TotalOrder` and `ExactlyOnce`.
3065    pub fn strict_kind(&self) -> CollectionKind {
3066        match self {
3067            CollectionKind::Stream {
3068                bound,
3069                element_type,
3070                ..
3071            } => CollectionKind::Stream {
3072                bound: bound.clone(),
3073                order: StreamOrder::TotalOrder,
3074                retry: StreamRetry::ExactlyOnce,
3075                element_type: element_type.clone(),
3076            },
3077            CollectionKind::KeyedStream {
3078                bound,
3079                key_type,
3080                value_type,
3081                ..
3082            } => CollectionKind::KeyedStream {
3083                bound: bound.clone(),
3084                value_order: StreamOrder::TotalOrder,
3085                value_retry: StreamRetry::ExactlyOnce,
3086                key_type: key_type.clone(),
3087                value_type: value_type.clone(),
3088            },
3089            other => other.clone(),
3090        }
3091    }
3092}
3093
3094#[derive(Clone, serde::Serialize)]
3095pub struct HydroIrMetadata {
3096    pub location_id: LocationId,
3097    pub collection_kind: CollectionKind,
3098    pub consistency: Option<ClusterConsistency>,
3099    pub cardinality: Option<usize>,
3100    pub tag: Option<String>,
3101    pub op: HydroIrOpMetadata,
3102}
3103
3104// HydroIrMetadata shouldn't be used to hash or compare
3105impl Hash for HydroIrMetadata {
3106    fn hash<H: Hasher>(&self, _: &mut H) {}
3107}
3108
3109impl PartialEq for HydroIrMetadata {
3110    fn eq(&self, _: &Self) -> bool {
3111        true
3112    }
3113}
3114
3115impl Eq for HydroIrMetadata {}
3116
3117impl Debug for HydroIrMetadata {
3118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3119        f.debug_struct("HydroIrMetadata")
3120            .field("location_id", &self.location_id)
3121            .field("collection_kind", &self.collection_kind)
3122            .finish()
3123    }
3124}
3125
3126/// Metadata that is specific to the operator itself, rather than its outputs.
3127/// This is available on _both_ inner nodes and roots.
3128#[derive(Clone, serde::Serialize)]
3129pub struct HydroIrOpMetadata {
3130    #[serde(rename = "span", serialize_with = "serialize_backtrace_as_span")]
3131    pub backtrace: Backtrace,
3132    pub cpu_usage: Option<f64>,
3133    pub network_recv_cpu_usage: Option<f64>,
3134    pub id: Option<usize>,
3135    /// When set, this unsafe operator (e.g. `batch` / `snapshot`) is bound to a simulator
3136    /// hook handle with this ID, letting simulation tests script its decisions. Ignored by
3137    /// non-simulator backends.
3138    #[serde(skip)]
3139    pub sim_hook_id: Option<usize>,
3140}
3141
3142impl HydroIrOpMetadata {
3143    #[expect(
3144        clippy::new_without_default,
3145        reason = "explicit calls to new ensure correct backtrace bounds"
3146    )]
3147    pub fn new() -> HydroIrOpMetadata {
3148        Self::new_with_skip(1)
3149    }
3150
3151    fn new_with_skip(skip_count: usize) -> HydroIrOpMetadata {
3152        HydroIrOpMetadata {
3153            backtrace: Backtrace::get_backtrace(2 + skip_count),
3154            cpu_usage: None,
3155            network_recv_cpu_usage: None,
3156            id: None,
3157            sim_hook_id: None,
3158        }
3159    }
3160}
3161
3162impl Debug for HydroIrOpMetadata {
3163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3164        f.debug_struct("HydroIrOpMetadata").finish()
3165    }
3166}
3167
3168impl Hash for HydroIrOpMetadata {
3169    fn hash<H: Hasher>(&self, _: &mut H) {}
3170}
3171
3172/// How a network channel's *sender* prepares each message before it is handed to the transport.
3173///
3174/// A channel's serialization is split into a send half ([`NetworkSend`]) and a receive half
3175/// ([`NetworkRecv`]) so that the multi-version simulation merge can reason about each side
3176/// independently (the sender fork and the receiver are separate IR nodes).
3177#[derive(Debug, Clone, Hash, serde::Serialize)]
3178pub enum NetworkSend {
3179    /// Serialization is performed within the Hydro dataflow using the provided serialize
3180    /// expression. This is how channels using [`crate::networking::Bincode`] are lowered.
3181    Custom { serialize_fn: Option<DebugExpr> },
3182    /// Serialization is left to code outside of Hydro (see [`crate::networking::Embedded`]). The
3183    /// raw `element_type` is passed through unserialized; the only transformation is converting a
3184    /// routing [`crate::location::MemberId`] (the destination cluster `tag`, when demuxing) into
3185    /// the raw `TaglessMemberId` used by the transport. Only supported by the embedded backend.
3186    ///
3187    /// Stored as structured info (rather than a pre-baked expression) so that the code can be
3188    /// synthesized in a post-IR codegen pass.
3189    Embedded {
3190        tag: Option<DebugType>,
3191        element_type: DebugType,
3192    },
3193}
3194
3195/// How a network channel's *receiver* recovers each message from the transport. See
3196/// [`NetworkSend`] for the sender half.
3197#[derive(Debug, Clone, Hash, serde::Serialize)]
3198pub enum NetworkRecv {
3199    /// Deserialization is performed within the Hydro dataflow using the provided deserialize
3200    /// expression. This is how channels using [`crate::networking::Bincode`] are lowered.
3201    Custom { deserialize_fn: Option<DebugExpr> },
3202    /// Deserialization is left to code outside of Hydro (see [`crate::networking::Embedded`]). The
3203    /// raw `element_type` is delivered to the receiver directly, with no transport `Result` to
3204    /// unwrap (the external code that produces the stream decides how to handle faults). The only
3205    /// transformation is converting a `TaglessMemberId` back into a typed
3206    /// [`crate::location::MemberId`] (the sender cluster `tag`, when the receiver is keyed by
3207    /// sender). Only supported by the embedded backend.
3208    Embedded {
3209        tag: Option<DebugType>,
3210        element_type: DebugType,
3211    },
3212}
3213
3214#[cfg(feature = "build")]
3215impl NetworkSend {
3216    /// The raw payload type flowing across the channel when serialization is left to external code,
3217    /// or [`None`] when the channel serializes internally.
3218    pub(crate) fn external_element_type(&self) -> Option<&syn::Type> {
3219        match self {
3220            NetworkSend::Custom { .. } => None,
3221            NetworkSend::Embedded { element_type, .. } => Some(&element_type.0),
3222        }
3223    }
3224}
3225
3226#[cfg(feature = "build")]
3227impl NetworkRecv {
3228    /// See [`NetworkSend::external_element_type`].
3229    pub(crate) fn external_element_type(&self) -> Option<&syn::Type> {
3230        match self {
3231            NetworkRecv::Custom { .. } => None,
3232            NetworkRecv::Embedded { element_type, .. } => Some(&element_type.0),
3233        }
3234    }
3235}
3236
3237#[cfg(feature = "build")]
3238impl NetworkSend {
3239    /// The expression applied on the sender to prepare each message for the transport, if any.
3240    pub(crate) fn pipeline(&self) -> Option<DebugExpr> {
3241        match self {
3242            NetworkSend::Custom { serialize_fn } => serialize_fn.clone(),
3243            NetworkSend::Embedded { tag, element_type } => {
3244                let root = crate::staging_util::get_this_crate();
3245                let element_type = &element_type.0;
3246                let expr: syn::Expr = if let Some(tag) = tag {
3247                    let tag = &tag.0;
3248                    parse_quote! {
3249                        #root::runtime_support::stageleft::runtime_support::fn1_type_hint::<(#root::__staged::location::MemberId<#tag>, #element_type), _>(
3250                            |(id, data)| (id.into_tagless(), data)
3251                        )
3252                    }
3253                } else {
3254                    parse_quote! {
3255                        #root::runtime_support::stageleft::runtime_support::fn1_type_hint::<#element_type, _>(
3256                            |data| data
3257                        )
3258                    }
3259                };
3260                Some(expr.into())
3261            }
3262        }
3263    }
3264}
3265
3266#[cfg(feature = "build")]
3267impl NetworkRecv {
3268    /// The expression applied on the receiver to recover each message from the transport, if any.
3269    pub(crate) fn pipeline(&self) -> Option<DebugExpr> {
3270        match self {
3271            NetworkRecv::Custom { deserialize_fn } => deserialize_fn.clone(),
3272            // Embedded channels hand the raw payload to the receiver directly (no transport
3273            // `Result`), so the developer's external code decides how to handle serialization
3274            // faults. The only transformation is restoring the typed `MemberId` when the receiver
3275            // is keyed by the sender.
3276            NetworkRecv::Embedded { tag, .. } => {
3277                let tag = tag.as_ref()?;
3278                let root = crate::staging_util::get_this_crate();
3279                let tag = &tag.0;
3280                let expr: syn::Expr = parse_quote! {
3281                    |(id, b)| (#root::__staged::location::MemberId::<#tag>::from_tagless(id as #root::__staged::location::TaglessMemberId), b)
3282                };
3283                Some(expr.into())
3284            }
3285        }
3286    }
3287}
3288
3289/// An intermediate node in a Hydro graph, which consumes data
3290/// from upstream nodes and emits data to downstream nodes.
3291#[derive(Debug, Hash, serde::Serialize)]
3292pub enum HydroNode {
3293    Placeholder,
3294
3295    /// Manually "casts" between two different collection kinds.
3296    ///
3297    /// Using this IR node requires special care, since it bypasses many of Hydro's core
3298    /// correctness checks. In particular, the user must ensure that every possible
3299    /// "interpretation" of the input corresponds to a distinct "interpretation" of the output,
3300    /// where an "interpretation" is a possible output of `ObserveNonDet` applied to the
3301    /// collection. This ensures that the simulator does not miss any possible outputs.
3302    Cast {
3303        inner: Box<HydroNode>,
3304        metadata: HydroIrMetadata,
3305    },
3306
3307    /// Strengthens the guarantees of a stream by non-deterministically selecting a possible
3308    /// interpretation of the input stream.
3309    ///
3310    /// In production, this simply passes through the input, but in simulation, this operator
3311    /// explicitly selects a randomized interpretation.
3312    ObserveNonDet {
3313        inner: Box<HydroNode>,
3314        trusted: bool, // if true, we do not need to simulate non-determinism
3315        metadata: HydroIrMetadata,
3316    },
3317
3318    Source {
3319        source: HydroSource,
3320        metadata: HydroIrMetadata,
3321    },
3322
3323    SingletonSource {
3324        value: DebugExpr,
3325        first_tick_only: bool,
3326        metadata: HydroIrMetadata,
3327    },
3328
3329    CycleSource {
3330        cycle_id: CycleId,
3331        metadata: HydroIrMetadata,
3332    },
3333
3334    Tee {
3335        inner: SharedNode,
3336        metadata: HydroIrMetadata,
3337    },
3338
3339    /// A reference materialization point. Wraps a `SharedNode` so that:
3340    /// - The pipe output delivers data to one consumer
3341    /// - `#var` references can borrow the value from the slot
3342    ///
3343    /// In DFIR codegen, emits `ident = inner_ident -> singleton()` or `-> optional()` or
3344    /// `-> handoff()` depending on `kind`.
3345    ///
3346    /// Uses the same `built_tees` dedup pattern as `Tee`.
3347    Reference {
3348        inner: SharedNode,
3349        kind: crate::handoff_ref::HandoffRefKind,
3350        access_counter: AccessCounter,
3351        metadata: HydroIrMetadata,
3352    },
3353
3354    /// An output side of the partition operator.
3355    PartitionSide {
3356        inner: SharedNode,
3357        is_true: bool,
3358        metadata: HydroIrMetadata,
3359    },
3360
3361    /// The inner input of partitioning, shared between two `PartitionSide`.
3362    PartitionShared {
3363        input: Box<HydroNode>,
3364        f: ClosureExpr,
3365        metadata: HydroIrMetadata,
3366    },
3367
3368    BeginAtomic {
3369        inner: Box<HydroNode>,
3370        metadata: HydroIrMetadata,
3371    },
3372
3373    EndAtomic {
3374        inner: Box<HydroNode>,
3375        metadata: HydroIrMetadata,
3376    },
3377
3378    /// Windows a stream-like collection (a stream, a keyed stream, or a keyed singleton whose
3379    /// per-key values are bounded) into a tick. Each firing of the tick observes a *batch* of
3380    /// the elements (or finalized entries) that have arrived since the previous batch, and
3381    /// never sees an element twice.
3382    ///
3383    /// The simulator fuzzes how the arriving elements are split into batches.
3384    Batch {
3385        inner: Box<HydroNode>,
3386        metadata: HydroIrMetadata,
3387    },
3388
3389    /// Windows a singleton-like collection (a singleton, an optional, or a keyed singleton
3390    /// whose per-key values may change, as well as a fully bounded keyed singleton) into a
3391    /// tick. Each firing of the tick observes a *snapshot* of the collection's current value,
3392    /// so a value that has not changed since the previous firing is observed again.
3393    ///
3394    /// The simulator fuzzes which of the collection's intermediate states each snapshot
3395    /// observes.
3396    Snapshot {
3397        inner: Box<HydroNode>,
3398        metadata: HydroIrMetadata,
3399    },
3400
3401    YieldConcat {
3402        inner: Box<HydroNode>,
3403        metadata: HydroIrMetadata,
3404    },
3405
3406    Chain {
3407        first: Box<HydroNode>,
3408        second: Box<HydroNode>,
3409        metadata: HydroIrMetadata,
3410    },
3411
3412    MergeOrdered {
3413        first: Box<HydroNode>,
3414        second: Box<HydroNode>,
3415        metadata: HydroIrMetadata,
3416    },
3417
3418    ChainFirst {
3419        first: Box<HydroNode>,
3420        second: Box<HydroNode>,
3421        metadata: HydroIrMetadata,
3422    },
3423
3424    CrossProduct {
3425        left: Box<HydroNode>,
3426        right: Box<HydroNode>,
3427        metadata: HydroIrMetadata,
3428    },
3429
3430    CrossSingleton {
3431        left: Box<HydroNode>,
3432        right: Box<HydroNode>,
3433        metadata: HydroIrMetadata,
3434    },
3435
3436    Join {
3437        left: Box<HydroNode>,
3438        right: Box<HydroNode>,
3439        metadata: HydroIrMetadata,
3440    },
3441
3442    /// Asymmetric join where the right (build) side is bounded.
3443    /// The build side is accumulated (stratum-delayed) into a hash table,
3444    /// then the left (probe) side streams through preserving its ordering.
3445    JoinHalf {
3446        left: Box<HydroNode>,
3447        right: Box<HydroNode>,
3448        metadata: HydroIrMetadata,
3449    },
3450
3451    Difference {
3452        pos: Box<HydroNode>,
3453        neg: Box<HydroNode>,
3454        metadata: HydroIrMetadata,
3455    },
3456
3457    AntiJoin {
3458        pos: Box<HydroNode>,
3459        neg: Box<HydroNode>,
3460        metadata: HydroIrMetadata,
3461    },
3462
3463    ResolveFutures {
3464        input: Box<HydroNode>,
3465        metadata: HydroIrMetadata,
3466    },
3467    ResolveFuturesBlocking {
3468        input: Box<HydroNode>,
3469        metadata: HydroIrMetadata,
3470    },
3471    ResolveFuturesOrdered {
3472        input: Box<HydroNode>,
3473        metadata: HydroIrMetadata,
3474    },
3475
3476    Map {
3477        f: ClosureExpr,
3478        input: Box<HydroNode>,
3479        metadata: HydroIrMetadata,
3480    },
3481    FlatMap {
3482        f: ClosureExpr,
3483        input: Box<HydroNode>,
3484        metadata: HydroIrMetadata,
3485    },
3486    FlatMapStreamBlocking {
3487        f: ClosureExpr,
3488        input: Box<HydroNode>,
3489        metadata: HydroIrMetadata,
3490    },
3491    Filter {
3492        f: ClosureExpr,
3493        input: Box<HydroNode>,
3494        metadata: HydroIrMetadata,
3495    },
3496    FilterMap {
3497        f: ClosureExpr,
3498        input: Box<HydroNode>,
3499        metadata: HydroIrMetadata,
3500    },
3501
3502    DeferTick {
3503        input: Box<HydroNode>,
3504        metadata: HydroIrMetadata,
3505    },
3506    Enumerate {
3507        input: Box<HydroNode>,
3508        metadata: HydroIrMetadata,
3509    },
3510    Inspect {
3511        f: ClosureExpr,
3512        input: Box<HydroNode>,
3513        metadata: HydroIrMetadata,
3514    },
3515
3516    Unique {
3517        input: Box<HydroNode>,
3518        metadata: HydroIrMetadata,
3519    },
3520
3521    Sort {
3522        input: Box<HydroNode>,
3523        metadata: HydroIrMetadata,
3524    },
3525    Fold {
3526        init: ClosureExpr,
3527        acc: ClosureExpr,
3528        input: Box<HydroNode>,
3529        metadata: HydroIrMetadata,
3530    },
3531
3532    Scan {
3533        init: ClosureExpr,
3534        acc: ClosureExpr,
3535        input: Box<HydroNode>,
3536        metadata: HydroIrMetadata,
3537    },
3538    ScanAsyncBlocking {
3539        init: ClosureExpr,
3540        acc: ClosureExpr,
3541        input: Box<HydroNode>,
3542        metadata: HydroIrMetadata,
3543    },
3544    FoldKeyed {
3545        init: ClosureExpr,
3546        acc: ClosureExpr,
3547        input: Box<HydroNode>,
3548        metadata: HydroIrMetadata,
3549    },
3550
3551    Reduce {
3552        f: ClosureExpr,
3553        input: Box<HydroNode>,
3554        metadata: HydroIrMetadata,
3555    },
3556    ReduceKeyed {
3557        f: ClosureExpr,
3558        input: Box<HydroNode>,
3559        metadata: HydroIrMetadata,
3560    },
3561    ReduceKeyedWatermark {
3562        f: ClosureExpr,
3563        input: Box<HydroNode>,
3564        watermark: Box<HydroNode>,
3565        metadata: HydroIrMetadata,
3566    },
3567
3568    Network {
3569        name: Option<String>,
3570        networking_info: crate::networking::NetworkingInfo,
3571        serialize: NetworkSend,
3572        deserialize: NetworkRecv,
3573        instantiate_fn: DebugInstantiate,
3574        input: Box<HydroNode>,
3575        metadata: HydroIrMetadata,
3576    },
3577
3578    VersionedNetworkFork {
3579        channel_id: u32,
3580        channel_name: String,
3581        senders: Vec<(u32, Box<HydroNode>, NetworkSend)>,
3582        metadata: HydroIrMetadata,
3583    },
3584
3585    VersionedNetwork {
3586        fork: SharedNode,
3587        version: u32,
3588        deserialize: NetworkRecv,
3589        metadata: HydroIrMetadata,
3590    },
3591
3592    ExternalInput {
3593        from_external_key: LocationKey,
3594        from_port_id: ExternalPortId,
3595        from_many: bool,
3596        codec_type: DebugType,
3597        #[serde(skip)]
3598        port_hint: NetworkHint,
3599        instantiate_fn: DebugInstantiate,
3600        deserialize_fn: Option<DebugExpr>,
3601        metadata: HydroIrMetadata,
3602    },
3603
3604    Counter {
3605        tag: String,
3606        duration: DebugExpr,
3607        prefix: String,
3608        input: Box<HydroNode>,
3609        metadata: HydroIrMetadata,
3610    },
3611
3612    AssertIsConsistent {
3613        inner: Box<HydroNode>,
3614        trusted: bool,
3615        metadata: HydroIrMetadata,
3616    },
3617
3618    UnboundSingleton {
3619        inner: Box<HydroNode>,
3620        metadata: HydroIrMetadata,
3621    },
3622}
3623
3624pub type SeenSharedNodes = HashMap<*const RefCell<HydroNode>, Rc<RefCell<HydroNode>>>;
3625pub type SeenSharedNodeLocations = HashMap<*const RefCell<HydroNode>, LocationId>;
3626
3627/// The runtime representation of a collection's DFIR pipe at a given node.
3628///
3629/// See [`CollectionKind::root_repr_is_tombstoned_update_feed`]: root-located unbounded
3630/// [`Optional`](CollectionKind::Optional)s and
3631/// [`KeyedSingleton`](CollectionKind::KeyedSingleton)s are represented as feeds of explicit
3632/// update records (`Option<T>` / `(K, Option<V>)`) so that *removal* is representable;
3633/// everything else is a plain feed of values.
3634#[cfg(feature = "build")]
3635#[derive(PartialEq, Eq, Clone, Copy, Debug)]
3636enum UpdateFeedRepr {
3637    /// Plain values (`T` elements / `(K, V)` entries).
3638    Plain,
3639    /// Tombstoned updates (`Option<T>` / `(K, Option<V>)`).
3640    Tombstoned,
3641}
3642
3643#[cfg(feature = "build")]
3644fn update_feed_repr(metadata: &HydroIrMetadata) -> UpdateFeedRepr {
3645    if metadata.location_id.is_root()
3646        && metadata
3647            .collection_kind
3648            .root_repr_is_tombstoned_update_feed()
3649    {
3650        UpdateFeedRepr::Tombstoned
3651    } else {
3652        UpdateFeedRepr::Plain
3653    }
3654}
3655
3656/// If `f` has a mut singleton ref and `in_kind` is non-strict, emits an
3657/// `observe_for_mut` node and returns the new ident. Otherwise returns
3658/// `in_ident` unchanged. Always consumes a `stmt_id` when applicable.
3659#[cfg(feature = "build")]
3660fn maybe_observe_for_mut(
3661    f: &ClosureExpr,
3662    in_ident: syn::Ident,
3663    in_location: &LocationId,
3664    in_kind: &CollectionKind,
3665    op_meta: &HydroIrOpMetadata,
3666    builders_or_callback: &mut BuildersOrCallback<
3667        '_,
3668        impl FnMut(&mut HydroRoot, &mut crate::Counter<StmtId>),
3669        impl FnMut(&mut HydroNode, &mut crate::Counter<StmtId>),
3670    >,
3671    next_stmt_id: &mut crate::Counter<StmtId>,
3672) -> syn::Ident {
3673    if f.has_mut_ref() && !in_kind.is_strict() {
3674        let observe_stmt_id = next_stmt_id.get_and_increment();
3675        let observe_ident =
3676            syn::Ident::new(&format!("stream_{}", observe_stmt_id), Span::call_site());
3677        if let BuildersOrCallback::Builders(graph_builders) = builders_or_callback {
3678            graph_builders.observe_for_mut(in_location, in_ident, in_kind, &observe_ident, op_meta);
3679        }
3680        observe_ident
3681    } else {
3682        in_ident
3683    }
3684}
3685
3686impl HydroNode {
3687    pub fn transform_bottom_up(
3688        &mut self,
3689        transform: &mut impl FnMut(&mut HydroNode),
3690        seen_tees: &mut SeenSharedNodes,
3691        check_well_formed: bool,
3692    ) {
3693        self.transform_children(
3694            |n, s| n.transform_bottom_up(transform, s, check_well_formed),
3695            seen_tees,
3696        );
3697
3698        transform(self);
3699
3700        let self_location = self.metadata().location_id.root();
3701
3702        if check_well_formed {
3703            match &*self {
3704                HydroNode::Network { .. } => {}
3705                _ => {
3706                    self.input_metadata().iter().for_each(|i| {
3707                        assert!(
3708                            i.location_id.root() == self_location,
3709                            "Mismatching IR locations, child: {:?} ({:?}) of: {:?} ({:?})",
3710                            i,
3711                            i.location_id.root(),
3712                            self,
3713                            self_location
3714                        );
3715                    });
3716                }
3717            }
3718        }
3719    }
3720
3721    #[inline(always)]
3722    pub fn transform_children(
3723        &mut self,
3724        mut transform: impl FnMut(&mut HydroNode, &mut SeenSharedNodes),
3725        seen_tees: &mut SeenSharedNodes,
3726    ) {
3727        match self {
3728            HydroNode::Placeholder => {
3729                panic!();
3730            }
3731
3732            HydroNode::Source { .. }
3733            | HydroNode::SingletonSource { .. }
3734            | HydroNode::CycleSource { .. }
3735            | HydroNode::ExternalInput { .. } => {}
3736
3737            HydroNode::Tee { inner, .. } | HydroNode::Reference { inner, .. } => {
3738                if let Some(transformed) = seen_tees.get(&inner.as_ptr()) {
3739                    *inner = SharedNode(transformed.clone());
3740                } else {
3741                    let transformed_cell = Rc::new(RefCell::new(HydroNode::Placeholder));
3742                    seen_tees.insert(inner.as_ptr(), transformed_cell.clone());
3743                    let mut orig = inner.0.replace(HydroNode::Placeholder);
3744                    transform(&mut orig, seen_tees);
3745                    *transformed_cell.borrow_mut() = orig;
3746                    *inner = SharedNode(transformed_cell);
3747                }
3748            }
3749
3750            HydroNode::PartitionSide { inner, .. } => {
3751                if let Some(transformed) = seen_tees.get(&inner.as_ptr()) {
3752                    *inner = SharedNode(transformed.clone());
3753                } else {
3754                    let transformed_cell = Rc::new(RefCell::new(HydroNode::Placeholder));
3755                    seen_tees.insert(inner.as_ptr(), transformed_cell.clone());
3756                    let mut orig: HydroNode = inner.0.replace(HydroNode::Placeholder);
3757                    transform(&mut orig, seen_tees);
3758                    *transformed_cell.borrow_mut() = orig;
3759                    *inner = SharedNode(transformed_cell);
3760                }
3761            }
3762            HydroNode::PartitionShared { input, f, .. } => {
3763                f.transform_children(&mut transform, seen_tees);
3764                transform(input.as_mut(), seen_tees);
3765            }
3766
3767            HydroNode::Cast { inner, .. }
3768            | HydroNode::ObserveNonDet { inner, .. }
3769            | HydroNode::BeginAtomic { inner, .. }
3770            | HydroNode::EndAtomic { inner, .. }
3771            | HydroNode::Batch { inner, .. }
3772            | HydroNode::Snapshot { inner, .. }
3773            | HydroNode::YieldConcat { inner, .. }
3774            | HydroNode::UnboundSingleton { inner, .. }
3775            | HydroNode::AssertIsConsistent { inner, .. } => {
3776                transform(inner.as_mut(), seen_tees);
3777            }
3778
3779            HydroNode::Chain { first, second, .. } => {
3780                transform(first.as_mut(), seen_tees);
3781                transform(second.as_mut(), seen_tees);
3782            }
3783
3784            HydroNode::MergeOrdered { first, second, .. } => {
3785                transform(first.as_mut(), seen_tees);
3786                transform(second.as_mut(), seen_tees);
3787            }
3788
3789            HydroNode::ChainFirst { first, second, .. } => {
3790                transform(first.as_mut(), seen_tees);
3791                transform(second.as_mut(), seen_tees);
3792            }
3793
3794            HydroNode::CrossSingleton { left, right, .. }
3795            | HydroNode::CrossProduct { left, right, .. }
3796            | HydroNode::Join { left, right, .. }
3797            | HydroNode::JoinHalf { left, right, .. } => {
3798                transform(left.as_mut(), seen_tees);
3799                transform(right.as_mut(), seen_tees);
3800            }
3801
3802            HydroNode::Difference { pos, neg, .. } | HydroNode::AntiJoin { pos, neg, .. } => {
3803                transform(pos.as_mut(), seen_tees);
3804                transform(neg.as_mut(), seen_tees);
3805            }
3806
3807            HydroNode::Map { f, input, .. } => {
3808                f.transform_children(&mut transform, seen_tees);
3809                transform(input.as_mut(), seen_tees);
3810            }
3811            HydroNode::FlatMap { f, input, .. }
3812            | HydroNode::FlatMapStreamBlocking { f, input, .. }
3813            | HydroNode::Filter { f, input, .. }
3814            | HydroNode::FilterMap { f, input, .. }
3815            | HydroNode::Inspect { f, input, .. }
3816            | HydroNode::Reduce { f, input, .. }
3817            | HydroNode::ReduceKeyed { f, input, .. } => {
3818                f.transform_children(&mut transform, seen_tees);
3819                transform(input.as_mut(), seen_tees);
3820            }
3821            HydroNode::ReduceKeyedWatermark {
3822                f,
3823                input,
3824                watermark,
3825                ..
3826            } => {
3827                f.transform_children(&mut transform, seen_tees);
3828                transform(input.as_mut(), seen_tees);
3829                transform(watermark.as_mut(), seen_tees);
3830            }
3831            HydroNode::Fold {
3832                init, acc, input, ..
3833            }
3834            | HydroNode::Scan {
3835                init, acc, input, ..
3836            }
3837            | HydroNode::ScanAsyncBlocking {
3838                init, acc, input, ..
3839            }
3840            | HydroNode::FoldKeyed {
3841                init, acc, input, ..
3842            } => {
3843                init.transform_children(&mut transform, seen_tees);
3844                acc.transform_children(&mut transform, seen_tees);
3845                transform(input.as_mut(), seen_tees);
3846            }
3847            HydroNode::ResolveFutures { input, .. }
3848            | HydroNode::ResolveFuturesBlocking { input, .. }
3849            | HydroNode::ResolveFuturesOrdered { input, .. }
3850            | HydroNode::Sort { input, .. }
3851            | HydroNode::DeferTick { input, .. }
3852            | HydroNode::Enumerate { input, .. }
3853            | HydroNode::Unique { input, .. }
3854            | HydroNode::Network { input, .. }
3855            | HydroNode::Counter { input, .. } => {
3856                transform(input.as_mut(), seen_tees);
3857            }
3858
3859            HydroNode::VersionedNetworkFork { senders, .. } => {
3860                for (_version, sender, _serialize) in senders.iter_mut() {
3861                    transform(sender.as_mut(), seen_tees);
3862                }
3863            }
3864
3865            HydroNode::VersionedNetwork { fork, .. } => {
3866                if let Some(transformed) = seen_tees.get(&fork.as_ptr()) {
3867                    *fork = SharedNode(transformed.clone());
3868                } else {
3869                    let transformed_cell = Rc::new(RefCell::new(HydroNode::Placeholder));
3870                    seen_tees.insert(fork.as_ptr(), transformed_cell.clone());
3871                    let mut orig = fork.0.replace(HydroNode::Placeholder);
3872                    transform(&mut orig, seen_tees);
3873                    *transformed_cell.borrow_mut() = orig;
3874                    *fork = SharedNode(transformed_cell);
3875                }
3876            }
3877        }
3878    }
3879
3880    pub fn deep_clone(&self, seen_tees: &mut SeenSharedNodes) -> HydroNode {
3881        match self {
3882            HydroNode::Placeholder => HydroNode::Placeholder,
3883            HydroNode::Cast { inner, metadata } => HydroNode::Cast {
3884                inner: Box::new(inner.deep_clone(seen_tees)),
3885                metadata: metadata.clone(),
3886            },
3887            HydroNode::UnboundSingleton { inner, metadata } => HydroNode::UnboundSingleton {
3888                inner: Box::new(inner.deep_clone(seen_tees)),
3889                metadata: metadata.clone(),
3890            },
3891            HydroNode::ObserveNonDet {
3892                inner,
3893                trusted,
3894                metadata,
3895            } => HydroNode::ObserveNonDet {
3896                inner: Box::new(inner.deep_clone(seen_tees)),
3897                trusted: *trusted,
3898                metadata: metadata.clone(),
3899            },
3900            HydroNode::AssertIsConsistent {
3901                inner,
3902                trusted,
3903                metadata,
3904            } => HydroNode::AssertIsConsistent {
3905                inner: Box::new(inner.deep_clone(seen_tees)),
3906                trusted: *trusted,
3907                metadata: metadata.clone(),
3908            },
3909            HydroNode::Source { source, metadata } => HydroNode::Source {
3910                source: source.clone(),
3911                metadata: metadata.clone(),
3912            },
3913            HydroNode::SingletonSource {
3914                value,
3915                first_tick_only,
3916                metadata,
3917            } => HydroNode::SingletonSource {
3918                value: value.clone(),
3919                first_tick_only: *first_tick_only,
3920                metadata: metadata.clone(),
3921            },
3922            HydroNode::CycleSource { cycle_id, metadata } => HydroNode::CycleSource {
3923                cycle_id: *cycle_id,
3924                metadata: metadata.clone(),
3925            },
3926            HydroNode::Tee { inner, metadata }
3927            | HydroNode::Reference {
3928                inner, metadata, ..
3929            } => {
3930                let cloned_inner = if let Some(transformed) = seen_tees.get(&inner.as_ptr()) {
3931                    SharedNode(transformed.clone())
3932                } else {
3933                    let new_rc = Rc::new(RefCell::new(HydroNode::Placeholder));
3934                    seen_tees.insert(inner.as_ptr(), new_rc.clone());
3935                    let cloned = inner.0.borrow().deep_clone(seen_tees);
3936                    *new_rc.borrow_mut() = cloned;
3937                    SharedNode(new_rc)
3938                };
3939                if let HydroNode::Reference {
3940                    kind,
3941                    access_counter,
3942                    ..
3943                } = self
3944                {
3945                    HydroNode::Reference {
3946                        inner: cloned_inner,
3947                        kind: *kind,
3948                        access_counter: access_counter.freeze(),
3949                        metadata: metadata.clone(),
3950                    }
3951                } else {
3952                    HydroNode::Tee {
3953                        inner: cloned_inner,
3954                        metadata: metadata.clone(),
3955                    }
3956                }
3957            }
3958            HydroNode::PartitionSide {
3959                inner,
3960                is_true,
3961                metadata,
3962            } => {
3963                if let Some(transformed) = seen_tees.get(&inner.as_ptr()) {
3964                    HydroNode::PartitionSide {
3965                        inner: SharedNode(transformed.clone()),
3966                        is_true: *is_true,
3967                        metadata: metadata.clone(),
3968                    }
3969                } else {
3970                    let new_rc = Rc::new(RefCell::new(HydroNode::Placeholder));
3971                    seen_tees.insert(inner.as_ptr(), new_rc.clone());
3972                    let cloned = inner.0.borrow().deep_clone(seen_tees);
3973                    *new_rc.borrow_mut() = cloned;
3974                    HydroNode::PartitionSide {
3975                        inner: SharedNode(new_rc),
3976                        is_true: *is_true,
3977                        metadata: metadata.clone(),
3978                    }
3979                }
3980            }
3981            HydroNode::PartitionShared { input, f, metadata } => HydroNode::PartitionShared {
3982                input: Box::new(input.deep_clone(seen_tees)),
3983                f: f.deep_clone(seen_tees),
3984                metadata: metadata.clone(),
3985            },
3986            HydroNode::YieldConcat { inner, metadata } => HydroNode::YieldConcat {
3987                inner: Box::new(inner.deep_clone(seen_tees)),
3988                metadata: metadata.clone(),
3989            },
3990            HydroNode::BeginAtomic { inner, metadata } => HydroNode::BeginAtomic {
3991                inner: Box::new(inner.deep_clone(seen_tees)),
3992                metadata: metadata.clone(),
3993            },
3994            HydroNode::EndAtomic { inner, metadata } => HydroNode::EndAtomic {
3995                inner: Box::new(inner.deep_clone(seen_tees)),
3996                metadata: metadata.clone(),
3997            },
3998            HydroNode::Batch { inner, metadata } => HydroNode::Batch {
3999                inner: Box::new(inner.deep_clone(seen_tees)),
4000                metadata: metadata.clone(),
4001            },
4002            HydroNode::Snapshot { inner, metadata } => HydroNode::Snapshot {
4003                inner: Box::new(inner.deep_clone(seen_tees)),
4004                metadata: metadata.clone(),
4005            },
4006            HydroNode::Chain {
4007                first,
4008                second,
4009                metadata,
4010            } => HydroNode::Chain {
4011                first: Box::new(first.deep_clone(seen_tees)),
4012                second: Box::new(second.deep_clone(seen_tees)),
4013                metadata: metadata.clone(),
4014            },
4015            HydroNode::MergeOrdered {
4016                first,
4017                second,
4018                metadata,
4019            } => HydroNode::MergeOrdered {
4020                first: Box::new(first.deep_clone(seen_tees)),
4021                second: Box::new(second.deep_clone(seen_tees)),
4022                metadata: metadata.clone(),
4023            },
4024            HydroNode::ChainFirst {
4025                first,
4026                second,
4027                metadata,
4028            } => HydroNode::ChainFirst {
4029                first: Box::new(first.deep_clone(seen_tees)),
4030                second: Box::new(second.deep_clone(seen_tees)),
4031                metadata: metadata.clone(),
4032            },
4033            HydroNode::CrossProduct {
4034                left,
4035                right,
4036                metadata,
4037            } => HydroNode::CrossProduct {
4038                left: Box::new(left.deep_clone(seen_tees)),
4039                right: Box::new(right.deep_clone(seen_tees)),
4040                metadata: metadata.clone(),
4041            },
4042            HydroNode::CrossSingleton {
4043                left,
4044                right,
4045                metadata,
4046            } => HydroNode::CrossSingleton {
4047                left: Box::new(left.deep_clone(seen_tees)),
4048                right: Box::new(right.deep_clone(seen_tees)),
4049                metadata: metadata.clone(),
4050            },
4051            HydroNode::Join {
4052                left,
4053                right,
4054                metadata,
4055            } => HydroNode::Join {
4056                left: Box::new(left.deep_clone(seen_tees)),
4057                right: Box::new(right.deep_clone(seen_tees)),
4058                metadata: metadata.clone(),
4059            },
4060            HydroNode::JoinHalf {
4061                left,
4062                right,
4063                metadata,
4064            } => HydroNode::JoinHalf {
4065                left: Box::new(left.deep_clone(seen_tees)),
4066                right: Box::new(right.deep_clone(seen_tees)),
4067                metadata: metadata.clone(),
4068            },
4069            HydroNode::Difference { pos, neg, metadata } => HydroNode::Difference {
4070                pos: Box::new(pos.deep_clone(seen_tees)),
4071                neg: Box::new(neg.deep_clone(seen_tees)),
4072                metadata: metadata.clone(),
4073            },
4074            HydroNode::AntiJoin { pos, neg, metadata } => HydroNode::AntiJoin {
4075                pos: Box::new(pos.deep_clone(seen_tees)),
4076                neg: Box::new(neg.deep_clone(seen_tees)),
4077                metadata: metadata.clone(),
4078            },
4079            HydroNode::ResolveFutures { input, metadata } => HydroNode::ResolveFutures {
4080                input: Box::new(input.deep_clone(seen_tees)),
4081                metadata: metadata.clone(),
4082            },
4083            HydroNode::ResolveFuturesBlocking { input, metadata } => {
4084                HydroNode::ResolveFuturesBlocking {
4085                    input: Box::new(input.deep_clone(seen_tees)),
4086                    metadata: metadata.clone(),
4087                }
4088            }
4089            HydroNode::ResolveFuturesOrdered { input, metadata } => {
4090                HydroNode::ResolveFuturesOrdered {
4091                    input: Box::new(input.deep_clone(seen_tees)),
4092                    metadata: metadata.clone(),
4093                }
4094            }
4095            HydroNode::Map { f, input, metadata } => HydroNode::Map {
4096                f: f.deep_clone(seen_tees),
4097                input: Box::new(input.deep_clone(seen_tees)),
4098                metadata: metadata.clone(),
4099            },
4100            HydroNode::FlatMap { f, input, metadata } => HydroNode::FlatMap {
4101                f: f.deep_clone(seen_tees),
4102                input: Box::new(input.deep_clone(seen_tees)),
4103                metadata: metadata.clone(),
4104            },
4105            HydroNode::FlatMapStreamBlocking { f, input, metadata } => {
4106                HydroNode::FlatMapStreamBlocking {
4107                    f: f.deep_clone(seen_tees),
4108                    input: Box::new(input.deep_clone(seen_tees)),
4109                    metadata: metadata.clone(),
4110                }
4111            }
4112            HydroNode::Filter { f, input, metadata } => HydroNode::Filter {
4113                f: f.deep_clone(seen_tees),
4114                input: Box::new(input.deep_clone(seen_tees)),
4115                metadata: metadata.clone(),
4116            },
4117            HydroNode::FilterMap { f, input, metadata } => HydroNode::FilterMap {
4118                f: f.deep_clone(seen_tees),
4119                input: Box::new(input.deep_clone(seen_tees)),
4120                metadata: metadata.clone(),
4121            },
4122            HydroNode::DeferTick { input, metadata } => HydroNode::DeferTick {
4123                input: Box::new(input.deep_clone(seen_tees)),
4124                metadata: metadata.clone(),
4125            },
4126            HydroNode::Enumerate { input, metadata } => HydroNode::Enumerate {
4127                input: Box::new(input.deep_clone(seen_tees)),
4128                metadata: metadata.clone(),
4129            },
4130            HydroNode::Inspect { f, input, metadata } => HydroNode::Inspect {
4131                f: f.deep_clone(seen_tees),
4132                input: Box::new(input.deep_clone(seen_tees)),
4133                metadata: metadata.clone(),
4134            },
4135            HydroNode::Unique { input, metadata } => HydroNode::Unique {
4136                input: Box::new(input.deep_clone(seen_tees)),
4137                metadata: metadata.clone(),
4138            },
4139            HydroNode::Sort { input, metadata } => HydroNode::Sort {
4140                input: Box::new(input.deep_clone(seen_tees)),
4141                metadata: metadata.clone(),
4142            },
4143            HydroNode::Fold {
4144                init,
4145                acc,
4146                input,
4147                metadata,
4148            } => HydroNode::Fold {
4149                init: init.deep_clone(seen_tees),
4150                acc: acc.deep_clone(seen_tees),
4151                input: Box::new(input.deep_clone(seen_tees)),
4152                metadata: metadata.clone(),
4153            },
4154            HydroNode::Scan {
4155                init,
4156                acc,
4157                input,
4158                metadata,
4159            } => HydroNode::Scan {
4160                init: init.deep_clone(seen_tees),
4161                acc: acc.deep_clone(seen_tees),
4162                input: Box::new(input.deep_clone(seen_tees)),
4163                metadata: metadata.clone(),
4164            },
4165            HydroNode::ScanAsyncBlocking {
4166                init,
4167                acc,
4168                input,
4169                metadata,
4170            } => HydroNode::ScanAsyncBlocking {
4171                init: init.deep_clone(seen_tees),
4172                acc: acc.deep_clone(seen_tees),
4173                input: Box::new(input.deep_clone(seen_tees)),
4174                metadata: metadata.clone(),
4175            },
4176            HydroNode::FoldKeyed {
4177                init,
4178                acc,
4179                input,
4180                metadata,
4181            } => HydroNode::FoldKeyed {
4182                init: init.deep_clone(seen_tees),
4183                acc: acc.deep_clone(seen_tees),
4184                input: Box::new(input.deep_clone(seen_tees)),
4185                metadata: metadata.clone(),
4186            },
4187            HydroNode::ReduceKeyedWatermark {
4188                f,
4189                input,
4190                watermark,
4191                metadata,
4192            } => HydroNode::ReduceKeyedWatermark {
4193                f: f.deep_clone(seen_tees),
4194                input: Box::new(input.deep_clone(seen_tees)),
4195                watermark: Box::new(watermark.deep_clone(seen_tees)),
4196                metadata: metadata.clone(),
4197            },
4198            HydroNode::Reduce { f, input, metadata } => HydroNode::Reduce {
4199                f: f.deep_clone(seen_tees),
4200                input: Box::new(input.deep_clone(seen_tees)),
4201                metadata: metadata.clone(),
4202            },
4203            HydroNode::ReduceKeyed { f, input, metadata } => HydroNode::ReduceKeyed {
4204                f: f.deep_clone(seen_tees),
4205                input: Box::new(input.deep_clone(seen_tees)),
4206                metadata: metadata.clone(),
4207            },
4208            HydroNode::Network {
4209                name,
4210                networking_info,
4211                serialize,
4212                deserialize,
4213                instantiate_fn,
4214                input,
4215                metadata,
4216            } => HydroNode::Network {
4217                name: name.clone(),
4218                networking_info: networking_info.clone(),
4219                serialize: serialize.clone(),
4220                deserialize: deserialize.clone(),
4221                instantiate_fn: instantiate_fn.clone(),
4222                input: Box::new(input.deep_clone(seen_tees)),
4223                metadata: metadata.clone(),
4224            },
4225            HydroNode::ExternalInput {
4226                from_external_key,
4227                from_port_id,
4228                from_many,
4229                codec_type,
4230                port_hint,
4231                instantiate_fn,
4232                deserialize_fn,
4233                metadata,
4234            } => HydroNode::ExternalInput {
4235                from_external_key: *from_external_key,
4236                from_port_id: *from_port_id,
4237                from_many: *from_many,
4238                codec_type: codec_type.clone(),
4239                port_hint: *port_hint,
4240                instantiate_fn: instantiate_fn.clone(),
4241                deserialize_fn: deserialize_fn.clone(),
4242                metadata: metadata.clone(),
4243            },
4244            HydroNode::Counter {
4245                tag,
4246                duration,
4247                prefix,
4248                input,
4249                metadata,
4250            } => HydroNode::Counter {
4251                tag: tag.clone(),
4252                duration: duration.clone(),
4253                prefix: prefix.clone(),
4254                input: Box::new(input.deep_clone(seen_tees)),
4255                metadata: metadata.clone(),
4256            },
4257            HydroNode::VersionedNetworkFork {
4258                channel_id,
4259                channel_name,
4260                senders,
4261                metadata,
4262            } => HydroNode::VersionedNetworkFork {
4263                channel_id: *channel_id,
4264                channel_name: channel_name.clone(),
4265                senders: senders
4266                    .iter()
4267                    .map(|(version, sender, serialize)| {
4268                        (
4269                            *version,
4270                            Box::new(sender.deep_clone(seen_tees)),
4271                            serialize.clone(),
4272                        )
4273                    })
4274                    .collect(),
4275                metadata: metadata.clone(),
4276            },
4277            HydroNode::VersionedNetwork {
4278                fork,
4279                version,
4280                deserialize,
4281                metadata,
4282            } => {
4283                let cloned_fork = if let Some(transformed) = seen_tees.get(&fork.as_ptr()) {
4284                    SharedNode(transformed.clone())
4285                } else {
4286                    let new_rc = Rc::new(RefCell::new(HydroNode::Placeholder));
4287                    seen_tees.insert(fork.as_ptr(), new_rc.clone());
4288                    let cloned = fork.0.borrow().deep_clone(seen_tees);
4289                    *new_rc.borrow_mut() = cloned;
4290                    SharedNode(new_rc)
4291                };
4292                HydroNode::VersionedNetwork {
4293                    fork: cloned_fork,
4294                    version: *version,
4295                    deserialize: deserialize.clone(),
4296                    metadata: metadata.clone(),
4297                }
4298            }
4299        }
4300    }
4301
4302    #[cfg(feature = "build")]
4303    pub fn emit_core(
4304        &mut self,
4305        builders_or_callback: &mut BuildersOrCallback<
4306            '_,
4307            impl FnMut(&mut HydroRoot, &mut crate::Counter<StmtId>),
4308            impl FnMut(&mut HydroNode, &mut crate::Counter<StmtId>),
4309        >,
4310        seen_tees: &mut SeenSharedNodes,
4311        built_tees: &mut HashMap<*const RefCell<HydroNode>, Vec<syn::Ident>>,
4312        next_stmt_id: &mut crate::Counter<StmtId>,
4313        fold_hooked_idents: &mut HashSet<String>,
4314    ) -> syn::Ident {
4315        let mut ident_stack: Vec<syn::Ident> = Vec::new();
4316
4317        self.transform_bottom_up(
4318            &mut |node: &mut HydroNode| {
4319                let out_location = node.metadata().location_id.clone();
4320                match node {
4321                    HydroNode::Placeholder => {
4322                        panic!()
4323                    }
4324
4325                    HydroNode::Cast { inner, metadata } => {
4326                        // Casts are metadata-only *except* when they cross the update-feed
4327                        // representation boundary (see
4328                        // [`CollectionKind::root_repr_is_tombstoned_update_feed`]):
4329                        // - Plain -> tombstoned (e.g. `ignore_init_none`, `into_optional` at
4330                        //   the root): every plain value becomes a `Some(v)` update (for keyed
4331                        //   feeds, every `(k, v)` entry becomes a `(k, Some(v))` upsert). This
4332                        //   is lossless: collections with a plain representation never become
4333                        //   absent, so no tombstone is ever needed.
4334                        // - Tombstoned -> plain (e.g. the cast of an always-present `or`
4335                        //   result back to a `Singleton` in `unwrap_or`/`zip`): keep the
4336                        //   values of `Some` updates. Tombstones are dropped, which is only
4337                        //   sound because such casts are constructed exclusively for feeds
4338                        //   whose consumers treat absence-of-value as absence anyway (the
4339                        //   plain-repr kinds have no notion of removal to lose).
4340                        // - Tombstoned -> tombstoned with *different* update-record encodings
4341                        //   (`Optional::into_keyed_singleton` at the root: `Option<(K, V)>`
4342                        //   updates -> `(K, Option<V>)` updates): a stateful scan remembers
4343                        //   the previously-emitted key so that when the optional becomes null
4344                        //   (or moves to a different key), the old key is explicitly
4345                        //   tombstoned.
4346                        let in_repr = update_feed_repr(inner.metadata());
4347                        let out_repr = update_feed_repr(metadata);
4348                        let cross_kind_tombstoned = in_repr == UpdateFeedRepr::Tombstoned
4349                            && out_repr == UpdateFeedRepr::Tombstoned
4350                            && std::mem::discriminant(&inner.metadata().collection_kind)
4351                                != std::mem::discriminant(&metadata.collection_kind);
4352
4353                        if in_repr == out_repr && !cross_kind_tombstoned {
4354                            // Identity: passes through the input ident unchanged. The input
4355                            // ident is already on the stack from processing the child.
4356                            let _ = next_stmt_id.get_and_increment();
4357                            match builders_or_callback {
4358                                BuildersOrCallback::Builders(_) => {}
4359                                BuildersOrCallback::Callback(_, node_callback) => {
4360                                    node_callback(node, next_stmt_id);
4361                                }
4362                            }
4363                            // input_ident stays on stack as output
4364                        } else {
4365                            let input_ident = ident_stack.pop().unwrap();
4366
4367                            let stmt_id = next_stmt_id.get_and_increment();
4368                            let cast_ident =
4369                                syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4370
4371                            match builders_or_callback {
4372                                BuildersOrCallback::Builders(graph_builders) => {
4373                                    let cast_op: TokenStream = if cross_kind_tombstoned {
4374                                        match (
4375                                            &inner.metadata().collection_kind,
4376                                            &metadata.collection_kind,
4377                                        ) {
4378                                            (
4379                                                CollectionKind::Optional { .. },
4380                                                CollectionKind::KeyedSingleton { .. },
4381                                            ) => {
4382                                                // `Optional::into_keyed_singleton` at the root.
4383                                                // Generated code requires `K: Clone + PartialEq`
4384                                                // (to compare against and tombstone the previous
4385                                                // key); these bounds are expressed on the
4386                                                // `into_keyed_singleton` API itself.
4387                                                let lifetime: TokenStream = graph_builders
4388                                                    .cross_tick_state_lifetime(&out_location);
4389                                                let lifetime: syn::Lifetime =
4390                                                    syn::parse2(lifetime).unwrap();
4391                                                quote! {
4392                                                    scan::<#lifetime>(
4393                                                        || ::std::option::Option::None,
4394                                                        |previous_key, update: ::std::option::Option<(_, _)>| {
4395                                                            let mut deltas = ::std::vec::Vec::new();
4396                                                            match update {
4397                                                                ::std::option::Option::Some((key, value)) => {
4398                                                                    match previous_key.take() {
4399                                                                        ::std::option::Option::Some(previous) if previous != key => {
4400                                                                            deltas.push((previous, ::std::option::Option::None));
4401                                                                        }
4402                                                                        _ => {}
4403                                                                    }
4404                                                                    *previous_key = ::std::option::Option::Some(::std::clone::Clone::clone(&key));
4405                                                                    deltas.push((key, ::std::option::Option::Some(value)));
4406                                                                }
4407                                                                ::std::option::Option::None => {
4408                                                                    if let ::std::option::Option::Some(previous) = previous_key.take() {
4409                                                                        deltas.push((previous, ::std::option::Option::None));
4410                                                                    }
4411                                                                }
4412                                                            }
4413                                                            ::std::option::Option::Some(deltas)
4414                                                        },
4415                                                    ) -> flatten()
4416                                                }
4417                                            }
4418                                            (in_kind, out_kind) => todo!(
4419                                                "casting between tombstoned update feeds \
4420                                                 {:?} -> {:?} is not yet supported",
4421                                                in_kind,
4422                                                out_kind
4423                                            ),
4424                                        }
4425                                    } else {
4426                                        match (in_repr, out_repr) {
4427                                            (UpdateFeedRepr::Plain, UpdateFeedRepr::Tombstoned) => {
4428                                                match &metadata.collection_kind {
4429                                                    CollectionKind::Optional { .. } => {
4430                                                        quote! { map(::std::option::Option::Some) }
4431                                                    }
4432                                                    CollectionKind::KeyedSingleton { .. } => {
4433                                                        quote! { map(|(__hydro_key, __hydro_value)| (__hydro_key, ::std::option::Option::Some(__hydro_value))) }
4434                                                    }
4435                                                    other => panic!(
4436                                                        "unexpected tombstoned update feed kind for cast: {:?}",
4437                                                        other
4438                                                    ),
4439                                                }
4440                                            }
4441                                            (UpdateFeedRepr::Tombstoned, UpdateFeedRepr::Plain) => {
4442                                                // Reached when a root unbounded optional is cast
4443                                                // back to a plain-repr kind: `Optional::unwrap_or`
4444                                                // (and its derivatives `into_singleton`,
4445                                                // `unwrap_or_default`, `is_some`, `is_none`) casts
4446                                                // the always-present `or` result to a `Singleton`,
4447                                                // and top-level `zip` casts the `latest()` result
4448                                                // back to a `Bounded` optional. Dropping
4449                                                // tombstones is sound only because these feeds
4450                                                // never become null after having a value, so no
4451                                                // tombstone is ever emitted at runtime.
4452                                                match &inner.metadata().collection_kind {
4453                                                    CollectionKind::Optional { .. } => {
4454                                                        quote! { filter_map(|__hydro_update| __hydro_update) }
4455                                                    }
4456                                                    CollectionKind::KeyedSingleton { .. } => {
4457                                                        quote! { filter_map(|(__hydro_key, __hydro_update)| __hydro_update.map(|__hydro_value| (__hydro_key, __hydro_value))) }
4458                                                    }
4459                                                    other => panic!(
4460                                                        "unexpected tombstoned update feed kind for cast: {:?}",
4461                                                        other
4462                                                    ),
4463                                                }
4464                                            }
4465                                            _ => unreachable!(),
4466                                        }
4467                                    };
4468
4469                                    graph_builders.add_dfir_at(
4470                                        &out_location,
4471                                        parse_quote! {
4472                                            #cast_ident = #input_ident -> #cast_op;
4473                                        },
4474                                        Some(&stmt_id.to_string()),
4475                                    );
4476                                }
4477                                BuildersOrCallback::Callback(_, node_callback) => {
4478                                    node_callback(node, next_stmt_id);
4479                                }
4480                            }
4481
4482                            ident_stack.push(cast_ident);
4483                        }
4484                    }
4485
4486                    HydroNode::UnboundSingleton { .. } => {
4487                        let inner_ident = ident_stack.pop().unwrap();
4488
4489                        let stmt_id = next_stmt_id.get_and_increment();
4490                        let out_ident =
4491                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4492
4493                        match builders_or_callback {
4494                            BuildersOrCallback::Builders(graph_builders) => {
4495                                if graph_builders.singleton_intermediates() {
4496                                    graph_builders.add_dfir_at(
4497                                        &out_location,
4498                                        parse_quote! {
4499                                            #out_ident = #inner_ident;
4500                                        },
4501                                        None,
4502                                    );
4503                                } else {
4504                                    graph_builders.add_dfir_at(
4505                                        &out_location,
4506                                        parse_quote! {
4507                                            #out_ident = #inner_ident -> persist::<'static>();
4508                                        },
4509                                        None,
4510                                    );
4511                                }
4512                            }
4513                            BuildersOrCallback::Callback(_, node_callback) => {
4514                                node_callback(node, next_stmt_id);
4515                            }
4516                        }
4517
4518                        ident_stack.push(out_ident);
4519                    }
4520
4521                    HydroNode::AssertIsConsistent { inner, trusted, .. } => {
4522                        let inner_ident = ident_stack.pop().unwrap();
4523
4524                        let stmt_id = next_stmt_id.get_and_increment();
4525                        let out_ident =
4526                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4527
4528                        match builders_or_callback {
4529                            BuildersOrCallback::Builders(graph_builders) => {
4530                                graph_builders.assert_is_consistent(
4531                                    *trusted,
4532                                    &inner.metadata().location_id,
4533                                    inner_ident,
4534                                    &out_ident,
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::ObserveNonDet {
4546                        inner,
4547                        trusted,
4548                        metadata,
4549                        ..
4550                    } => {
4551                        let inner_ident = ident_stack.pop().unwrap();
4552
4553                        let stmt_id = next_stmt_id.get_and_increment();
4554                        let observe_ident =
4555                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4556
4557                        match builders_or_callback {
4558                            BuildersOrCallback::Builders(graph_builders) => {
4559                                graph_builders.observe_nondet(
4560                                    *trusted,
4561                                    &inner.metadata().location_id,
4562                                    inner_ident,
4563                                    &inner.metadata().collection_kind,
4564                                    &observe_ident,
4565                                    &metadata.collection_kind,
4566                                    &metadata.op,
4567                                );
4568                            }
4569                            BuildersOrCallback::Callback(_, node_callback) => {
4570                                node_callback(node, next_stmt_id);
4571                            }
4572                        }
4573
4574                        ident_stack.push(observe_ident);
4575                    }
4576
4577                    HydroNode::Batch {
4578                        inner, metadata, ..
4579                    } => {
4580                        let inner_ident = ident_stack.pop().unwrap();
4581
4582                        let stmt_id = next_stmt_id.get_and_increment();
4583                        let batch_ident =
4584                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4585
4586                        match builders_or_callback {
4587                            BuildersOrCallback::Builders(graph_builders) => {
4588                                graph_builders.batch(
4589                                    inner_ident,
4590                                    &inner.metadata().location_id,
4591                                    &inner.metadata().collection_kind,
4592                                    &batch_ident,
4593                                    &out_location,
4594                                    &metadata.op,
4595                                    fold_hooked_idents,
4596                                );
4597                            }
4598                            BuildersOrCallback::Callback(_, node_callback) => {
4599                                node_callback(node, next_stmt_id);
4600                            }
4601                        }
4602
4603                        ident_stack.push(batch_ident);
4604                    }
4605
4606                    HydroNode::Snapshot {
4607                        inner, metadata, ..
4608                    } => {
4609                        let inner_ident = ident_stack.pop().unwrap();
4610
4611                        let stmt_id = next_stmt_id.get_and_increment();
4612                        let snapshot_ident =
4613                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4614
4615                        match builders_or_callback {
4616                            BuildersOrCallback::Builders(graph_builders) => {
4617                                graph_builders.snapshot(
4618                                    inner_ident,
4619                                    &inner.metadata().location_id,
4620                                    &inner.metadata().collection_kind,
4621                                    &snapshot_ident,
4622                                    &out_location,
4623                                    &metadata.op,
4624                                    fold_hooked_idents,
4625                                );
4626                            }
4627                            BuildersOrCallback::Callback(_, node_callback) => {
4628                                node_callback(node, next_stmt_id);
4629                            }
4630                        }
4631
4632                        ident_stack.push(snapshot_ident);
4633                    }
4634
4635                    HydroNode::YieldConcat {
4636                        inner, metadata, ..
4637                    } => {
4638                        let inner_ident = ident_stack.pop().unwrap();
4639
4640                        let stmt_id = next_stmt_id.get_and_increment();
4641                        let yield_ident =
4642                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4643
4644                        match builders_or_callback {
4645                            BuildersOrCallback::Builders(graph_builders) => {
4646                                graph_builders.yield_from_tick(
4647                                    inner_ident,
4648                                    &inner.metadata().location_id,
4649                                    &inner.metadata().collection_kind,
4650                                    &yield_ident,
4651                                    &out_location,
4652                                    &metadata.collection_kind,
4653                                );
4654                            }
4655                            BuildersOrCallback::Callback(_, node_callback) => {
4656                                node_callback(node, next_stmt_id);
4657                            }
4658                        }
4659
4660                        ident_stack.push(yield_ident);
4661                    }
4662
4663                    HydroNode::BeginAtomic { inner, metadata } => {
4664                        let inner_ident = ident_stack.pop().unwrap();
4665
4666                        let stmt_id = next_stmt_id.get_and_increment();
4667                        let begin_ident =
4668                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4669
4670                        match builders_or_callback {
4671                            BuildersOrCallback::Builders(graph_builders) => {
4672                                graph_builders.begin_atomic(
4673                                    inner_ident,
4674                                    &inner.metadata().location_id,
4675                                    &inner.metadata().collection_kind,
4676                                    &begin_ident,
4677                                    &out_location,
4678                                    &metadata.op,
4679                                );
4680                            }
4681                            BuildersOrCallback::Callback(_, node_callback) => {
4682                                node_callback(node, next_stmt_id);
4683                            }
4684                        }
4685
4686                        ident_stack.push(begin_ident);
4687                    }
4688
4689                    HydroNode::EndAtomic {
4690                        inner, metadata, ..
4691                    } => {
4692                        let inner_ident = ident_stack.pop().unwrap();
4693
4694                        let stmt_id = next_stmt_id.get_and_increment();
4695                        let end_ident =
4696                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4697
4698                        match builders_or_callback {
4699                            BuildersOrCallback::Builders(graph_builders) => {
4700                                graph_builders.end_atomic(
4701                                    inner_ident,
4702                                    &inner.metadata().location_id,
4703                                    &inner.metadata().collection_kind,
4704                                    &end_ident,
4705                                    &metadata.collection_kind,
4706                                );
4707                            }
4708                            BuildersOrCallback::Callback(_, node_callback) => {
4709                                node_callback(node, next_stmt_id);
4710                            }
4711                        }
4712
4713                        ident_stack.push(end_ident);
4714                    }
4715
4716                    HydroNode::Source {
4717                        source, metadata, ..
4718                    } => {
4719                        if matches!(source, HydroSource::ExternalNetwork()) {
4720                            ident_stack.push(syn::Ident::new("DUMMY", Span::call_site()));
4721                        } else {
4722                            let stmt_id = next_stmt_id.get_and_increment();
4723                            let source_ident =
4724                                syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4725
4726                            // For tick-located sources, holds the source pipeline (RHS) so that
4727                            // production codegen can hoist it to the root and window it into the
4728                            // tick's loop (DFIR sources may not live inside a `loop { ... }`).
4729                            let mut tick_source_rhs: Option<TokenStream> = None;
4730
4731                            let source_stmt = match source {
4732                                HydroSource::Stream(expr) => {
4733                                    debug_assert!(metadata.location_id.is_top_level());
4734                                    parse_quote! {
4735                                        #source_ident = source_stream(#expr);
4736                                    }
4737                                }
4738
4739                                HydroSource::ExternalNetwork() => {
4740                                    unreachable!()
4741                                }
4742
4743                                HydroSource::Iter(expr) => {
4744                                    if metadata.location_id.is_root() {
4745                                        parse_quote! {
4746                                            #source_ident = source_iter(#expr);
4747                                        }
4748                                    } else {
4749                                        // Located inside a tick or atomic region (which is fused
4750                                        // into its tick's loop). DFIR sources may not live inside a
4751                                        // `loop { ... }`, so hoist the source to the root and window
4752                                        // it into the loop.
4753                                        // TODO(shadaj): a more natural semantics would be to re-evaluate the expression on each tick
4754                                        tick_source_rhs = Some(quote! { source_iter(#expr) });
4755                                        parse_quote! {
4756                                            #source_ident = source_iter(#expr) -> persist::<'static>();
4757                                        }
4758                                    }
4759                                }
4760
4761                                HydroSource::Spin() => {
4762                                    debug_assert!(metadata.location_id.is_top_level());
4763                                    parse_quote! {
4764                                        #source_ident = spin();
4765                                    }
4766                                }
4767
4768                                HydroSource::ClusterMembers(target_loc, state) => {
4769                                    debug_assert!(metadata.location_id.is_top_level());
4770
4771                                    let members_tee_ident = syn::Ident::new(
4772                                        &format!(
4773                                            "__cluster_members_tee_{}_{}",
4774                                            metadata.location_id.root().key(),
4775                                            target_loc.key(),
4776                                        ),
4777                                        Span::call_site(),
4778                                    );
4779
4780                                    match state {
4781                                        ClusterMembersState::Stream(d) => {
4782                                            parse_quote! {
4783                                                #members_tee_ident = source_stream(#d) -> tee();
4784                                                #source_ident = #members_tee_ident;
4785                                            }
4786                                        },
4787                                        ClusterMembersState::Uninit => syn::parse_quote! {
4788                                            #source_ident = source_stream(DUMMY);
4789                                        },
4790                                        ClusterMembersState::Tee(..) => parse_quote! {
4791                                            #source_ident = #members_tee_ident;
4792                                        },
4793                                    }
4794                                }
4795
4796                                HydroSource::Embedded(ident) => {
4797                                    parse_quote! {
4798                                        #source_ident = source_stream(#ident);
4799                                    }
4800                                }
4801
4802                                HydroSource::EmbeddedSingleton(ident) => {
4803                                    parse_quote! {
4804                                        #source_ident = source_iter([#ident]);
4805                                    }
4806                                }
4807                            };
4808
4809                            match builders_or_callback {
4810                                BuildersOrCallback::Builders(graph_builders) => {
4811                                    if let Some(source_rhs) = tick_source_rhs {
4812                                        graph_builders.add_tick_source(
4813                                            &out_location,
4814                                            source_rhs,
4815                                            &source_ident,
4816                                            true,
4817                                            Some(&stmt_id.to_string()),
4818                                        );
4819                                    } else {
4820                                        graph_builders.add_dfir_at(
4821                                            &out_location,
4822                                            source_stmt,
4823                                            Some(&stmt_id.to_string()),
4824                                        );
4825                                    }
4826                                }
4827                                BuildersOrCallback::Callback(_, node_callback) => {
4828                                    node_callback(node, next_stmt_id);
4829                                }
4830                            }
4831
4832                            ident_stack.push(source_ident);
4833                        }
4834                    }
4835
4836                    HydroNode::SingletonSource { value, first_tick_only, metadata } => {
4837                        let stmt_id = next_stmt_id.get_and_increment();
4838                        let source_ident =
4839                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4840
4841                        match builders_or_callback {
4842                            BuildersOrCallback::Builders(graph_builders) => {
4843                                if *first_tick_only {
4844                                    assert!(
4845                                        !metadata.location_id.is_top_level(),
4846                                        "first_tick_only SingletonSource must be inside a tick"
4847                                    );
4848                                    // Delivered only on the first execution of the tick region.
4849                                    graph_builders.add_tick_source(
4850                                        &out_location,
4851                                        quote! { source_iter([#value]) },
4852                                        &source_ident,
4853                                        false,
4854                                        Some(&stmt_id.to_string()),
4855                                    );
4856                                } else if metadata.location_id.is_root() {
4857                                    if metadata.collection_kind.is_bounded() {
4858                                        graph_builders.add_dfir_at(
4859                                            &out_location,
4860                                            parse_quote! {
4861                                                #source_ident = source_iter([#value]);
4862                                            },
4863                                            Some(&stmt_id.to_string()),
4864                                        );
4865                                    } else {
4866                                        graph_builders.add_dfir_at(
4867                                            &out_location,
4868                                            parse_quote! {
4869                                                #source_ident = source_iter([#value]) -> persist::<'static>();
4870                                            },
4871                                            Some(&stmt_id.to_string()),
4872                                        );
4873                                    }
4874                                } else {
4875                                    // A tick- or atomic-located singleton source (the atomic
4876                                    // region is fused into its tick's loop) that yields its value
4877                                    // on every firing. DFIR sources may not live inside a
4878                                    // `loop { ... }`, so hoist it to the root and window it in.
4879                                    graph_builders.add_tick_source(
4880                                        &out_location,
4881                                        quote! { source_iter([#value]) },
4882                                        &source_ident,
4883                                        true,
4884                                        Some(&stmt_id.to_string()),
4885                                    );
4886                                }
4887                            }
4888                            BuildersOrCallback::Callback(_, node_callback) => {
4889                                node_callback(node, next_stmt_id);
4890                            }
4891                        }
4892
4893                        ident_stack.push(source_ident);
4894                    }
4895
4896                    HydroNode::CycleSource { cycle_id, .. } => {
4897                        let ident = cycle_id.as_ident();
4898
4899                        // consume a stmt id even though we did not emit anything so that we can instrument this
4900                        let _ = next_stmt_id.get_and_increment();
4901
4902                        match builders_or_callback {
4903                            BuildersOrCallback::Builders(_) => {}
4904                            BuildersOrCallback::Callback(_, node_callback) => {
4905                                node_callback(node, next_stmt_id);
4906                            }
4907                        }
4908
4909                        ident_stack.push(ident);
4910                    }
4911
4912                    HydroNode::Tee { inner, .. } => {
4913                        // we consume a stmt id regardless of if we emit the tee() operator,
4914                        // so that during rewrites we touch all recipients of the tee()
4915                        let stmt_id = next_stmt_id.get_and_increment();
4916
4917                        let ret_ident = if let Some(built_idents) =
4918                            built_tees.get(&(std::ptr::from_ref(inner.0.as_ref())))
4919                        {
4920                            match builders_or_callback {
4921                                BuildersOrCallback::Builders(_) => {}
4922                                BuildersOrCallback::Callback(_, node_callback) => {
4923                                    node_callback(node, next_stmt_id);
4924                                }
4925                            }
4926
4927                            built_idents[0].clone()
4928                        } else {
4929                            // The inner node was already processed by transform_bottom_up,
4930                            // so its ident is on the stack
4931                            let inner_ident = ident_stack.pop().unwrap();
4932
4933                            let tee_ident =
4934                                syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4935
4936                            built_tees.insert(
4937                                std::ptr::from_ref(inner.0.as_ref()),
4938                                vec![tee_ident.clone()],
4939                            );
4940
4941                            match builders_or_callback {
4942                                BuildersOrCallback::Builders(graph_builders) => {
4943                                    // NOTE: With `forward_ref`, the fold codegen may not have
4944                                    // run yet when we reach this tee, so `fold_hooked_idents`
4945                                    // might not contain the inner ident. In that case we won't
4946                                    // propagate the "hooked" status to the tee and the
4947                                    // downstream singleton batch will use the normal
4948                                    // `SingletonHook` instead of `PassthroughSingletonHook`.
4949                                    // This is not a soundness issue: the fallback hook still
4950                                    // produces correct behavior, just with a redundant decision
4951                                    // point. TODO(https://github.com/hydro-project/hydro/issues/2856):
4952                                    // fix ordering so forward_ref folds are always processed
4953                                    // before their downstream tees.
4954                                    if fold_hooked_idents.contains(&inner_ident.to_string()) {
4955                                        fold_hooked_idents.insert(tee_ident.to_string());
4956                                    }
4957                                    graph_builders.add_dfir_at(
4958                                        &out_location,
4959                                        parse_quote! {
4960                                            #tee_ident = #inner_ident -> tee();
4961                                        },
4962                                        Some(&stmt_id.to_string()),
4963                                    );
4964                                }
4965                                BuildersOrCallback::Callback(_, node_callback) => {
4966                                    node_callback(node, next_stmt_id);
4967                                }
4968                            }
4969
4970                            tee_ident
4971                        };
4972
4973                        ident_stack.push(ret_ident);
4974                    }
4975
4976                    HydroNode::Reference { inner, kind, .. } => {
4977                        // we consume a stmt id regardless of if we emit the operator,
4978                        // so that during rewrites we touch all recipients
4979                        let stmt_id = next_stmt_id.get_and_increment();
4980
4981                        let ret_ident = if let Some(built_idents) =
4982                            built_tees.get(&(std::ptr::from_ref(inner.0.as_ref())))
4983                        {
4984                            built_idents[0].clone()
4985                        } else {
4986                            let inner_ident = ident_stack.pop().unwrap();
4987
4988                            let ref_ident =
4989                                syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
4990
4991                            built_tees.insert(
4992                                std::ptr::from_ref(inner.0.as_ref()),
4993                                vec![ref_ident.clone()],
4994                            );
4995
4996                            match builders_or_callback {
4997                                BuildersOrCallback::Builders(graph_builders) => {
4998                                    let op_ident = syn::Ident::new(
4999                                        match kind {
5000                                            crate::handoff_ref::HandoffRefKind::Singleton => "singleton",
5001                                            crate::handoff_ref::HandoffRefKind::Optional => "optional",
5002                                            crate::handoff_ref::HandoffRefKind::Vec => "handoff",
5003                                        },
5004                                        Span::call_site(),
5005                                    );
5006                                    graph_builders.add_dfir_at(
5007                                        &out_location,
5008                                        parse_quote! {
5009                                            #ref_ident = #inner_ident -> #op_ident();
5010                                        },
5011                                        Some(&stmt_id.to_string()),
5012                                    );
5013                                }
5014                                BuildersOrCallback::Callback(_, node_callback) => {
5015                                    node_callback(node, next_stmt_id);
5016                                }
5017                            }
5018
5019                            ref_ident
5020                        };
5021
5022                        ident_stack.push(ret_ident);
5023                    }
5024
5025                    HydroNode::PartitionSide {
5026                        inner, is_true, metadata: _,
5027                    } => {
5028                        let is_true = *is_true; // need to copy early to avoid borrow checking issues with node
5029                        let ptr = std::ptr::from_ref(inner.0.as_ref());
5030                        let stmt_id = next_stmt_id.get_and_increment();
5031
5032                        let ret_ident = if let Some(built_idents) = built_tees.get(&ptr) {
5033                            match builders_or_callback {
5034                                BuildersOrCallback::Builders(_) => {}
5035                                BuildersOrCallback::Callback(_, node_callback) => {
5036                                    node_callback(node, next_stmt_id);
5037                                }
5038                            }
5039
5040                            let idx = usize::from(!is_true);
5041                            built_idents[idx].clone()
5042                        } else {
5043                            // The `PartitionShared` node was already processed by transform_bottom_up,
5044                            // so its ident is on the stack
5045                            let partition_ident = ident_stack.pop().unwrap();
5046
5047                            let true_ident = syn::Ident::new(
5048                                &format!("stream_{}_true", stmt_id),
5049                                Span::call_site(),
5050                            );
5051                            let false_ident = syn::Ident::new(
5052                                &format!("stream_{}_false", stmt_id),
5053                                Span::call_site(),
5054                            );
5055
5056                            built_tees.insert(
5057                                ptr,
5058                                vec![true_ident.clone(), false_ident.clone()],
5059                            );
5060
5061                            let stmt_id = next_stmt_id.get_and_increment();
5062                            match builders_or_callback {
5063                                BuildersOrCallback::Builders(graph_builders) => {
5064                                    graph_builders.add_dfir_at(
5065                                        &out_location,
5066                                        parse_quote! {
5067                                            #true_ident = #partition_ident[0];
5068                                            #false_ident = #partition_ident[1];
5069                                        },
5070                                        Some(&stmt_id.to_string()),
5071                                    );
5072                                }
5073                                BuildersOrCallback::Callback(_, node_callback) => {
5074                                    node_callback(node, next_stmt_id);
5075                                }
5076                            }
5077
5078                            if is_true { true_ident } else { false_ident }
5079                        };
5080
5081                        ident_stack.push(ret_ident);
5082                    }
5083
5084                    HydroNode::PartitionShared { input, f, metadata } => {
5085                        // Pop input ident (pushed last by transform_children) before
5086                        // draining the closure's singleton ref idents below it.
5087                        let inner_ident = ident_stack.pop().unwrap();
5088                        let f_tokens = f.emit_tokens(&mut ident_stack);
5089
5090                        let inner_ident = {
5091                            maybe_observe_for_mut(
5092                                f, inner_ident,
5093                                &input.metadata().location_id,
5094                                &input.metadata().collection_kind,
5095                                &metadata.op,
5096                                builders_or_callback, next_stmt_id,
5097                            )
5098                        };
5099
5100                        let stmt_id = next_stmt_id.get_and_increment();
5101                        let partition_ident = syn::Ident::new(
5102                            &format!("stream_{}_partition", stmt_id),
5103                            Span::call_site(),
5104                        );
5105
5106                        let stmt_id = next_stmt_id.get_and_increment();
5107                        match builders_or_callback {
5108                            BuildersOrCallback::Builders(graph_builders) => {
5109                                graph_builders.add_dfir_at(
5110                                    &out_location,
5111                                    parse_quote! {
5112                                        #partition_ident = #inner_ident -> partition(|__item, __num_outputs| if (#f_tokens)(__item) { 0_usize } else { 1_usize });
5113                                    },
5114                                    Some(&stmt_id.to_string()),
5115                                );
5116                            }
5117                            BuildersOrCallback::Callback(_, node_callback) => {
5118                                node_callback(node, next_stmt_id);
5119                            }
5120                        }
5121                        ident_stack.push(partition_ident);
5122                    }
5123
5124                    HydroNode::Chain { .. } => {
5125                        // Children are processed left-to-right, so second is on top
5126                        let second_ident = ident_stack.pop().unwrap();
5127                        let first_ident = ident_stack.pop().unwrap();
5128
5129                        let stmt_id = next_stmt_id.get_and_increment();
5130                        let chain_ident =
5131                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5132
5133                        match builders_or_callback {
5134                            BuildersOrCallback::Builders(graph_builders) => {
5135                                graph_builders.add_dfir_at(
5136                                    &out_location,
5137                                    parse_quote! {
5138                                        #chain_ident = chain();
5139                                        #first_ident -> [0]#chain_ident;
5140                                        #second_ident -> [1]#chain_ident;
5141                                    },
5142                                    Some(&stmt_id.to_string()),
5143                                );
5144                            }
5145                            BuildersOrCallback::Callback(_, node_callback) => {
5146                                node_callback(node, next_stmt_id);
5147                            }
5148                        }
5149
5150                        ident_stack.push(chain_ident);
5151                    }
5152
5153                    HydroNode::MergeOrdered { first, metadata, .. } => {
5154                        let second_ident = ident_stack.pop().unwrap();
5155                        let first_ident = ident_stack.pop().unwrap();
5156
5157                        let stmt_id = next_stmt_id.get_and_increment();
5158                        let merge_ident =
5159                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5160
5161                        match builders_or_callback {
5162                            BuildersOrCallback::Builders(graph_builders) => {
5163                                graph_builders.merge_ordered(
5164                                    &first.metadata().location_id,
5165                                    first_ident,
5166                                    second_ident,
5167                                    &merge_ident,
5168                                    &first.metadata().collection_kind,
5169                                    &metadata.op,
5170                                    Some(&stmt_id.to_string()),
5171                                );
5172                            }
5173                            BuildersOrCallback::Callback(_, node_callback) => {
5174                                node_callback(node, next_stmt_id);
5175                            }
5176                        }
5177
5178                        ident_stack.push(merge_ident);
5179                    }
5180
5181                    HydroNode::ChainFirst { .. } => {
5182                        let second_ident = ident_stack.pop().unwrap();
5183                        let first_ident = ident_stack.pop().unwrap();
5184
5185                        let stmt_id = next_stmt_id.get_and_increment();
5186                        let chain_ident =
5187                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5188
5189                        match builders_or_callback {
5190                            BuildersOrCallback::Builders(graph_builders) => {
5191                                graph_builders.add_dfir_at(
5192                                    &out_location,
5193                                    parse_quote! {
5194                                        #chain_ident = chain_first_n(1);
5195                                        #first_ident -> [0]#chain_ident;
5196                                        #second_ident -> [1]#chain_ident;
5197                                    },
5198                                    Some(&stmt_id.to_string()),
5199                                );
5200                            }
5201                            BuildersOrCallback::Callback(_, node_callback) => {
5202                                node_callback(node, next_stmt_id);
5203                            }
5204                        }
5205
5206                        ident_stack.push(chain_ident);
5207                    }
5208
5209                    HydroNode::CrossSingleton { right, .. } => {
5210                        let right_ident = ident_stack.pop().unwrap();
5211                        let left_ident = ident_stack.pop().unwrap();
5212
5213                        let stmt_id = next_stmt_id.get_and_increment();
5214                        let cross_ident =
5215                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5216
5217                        match builders_or_callback {
5218                            BuildersOrCallback::Builders(graph_builders) => {
5219                                if right.metadata().location_id.is_top_level()
5220                                    && right.metadata().collection_kind.is_bounded()
5221                                {
5222                                    let lifetime =
5223                                        graph_builders.cross_tick_state_lifetime(&out_location);
5224                                    graph_builders.add_dfir_at(
5225                                        &out_location,
5226                                        parse_quote! {
5227                                            #cross_ident = cross_singleton::<#lifetime>();
5228                                            #left_ident -> [input]#cross_ident;
5229                                            #right_ident -> [single]#cross_ident;
5230                                        },
5231                                        Some(&stmt_id.to_string()),
5232                                    );
5233                                } else {
5234                                    graph_builders.add_dfir_at(
5235                                        &out_location,
5236                                        parse_quote! {
5237                                            #cross_ident = cross_singleton();
5238                                            #left_ident -> [input]#cross_ident;
5239                                            #right_ident -> [single]#cross_ident;
5240                                        },
5241                                        Some(&stmt_id.to_string()),
5242                                    );
5243                                }
5244                            }
5245                            BuildersOrCallback::Callback(_, node_callback) => {
5246                                node_callback(node, next_stmt_id);
5247                            }
5248                        }
5249
5250                        ident_stack.push(cross_ident);
5251                    }
5252
5253                    HydroNode::CrossProduct { .. } | HydroNode::Join { .. } => {
5254                        let operator: syn::Ident = if matches!(node, HydroNode::CrossProduct { .. }) {
5255                            parse_quote!(cross_join_multiset)
5256                        } else {
5257                            parse_quote!(join_multiset)
5258                        };
5259
5260                        let (HydroNode::CrossProduct { left, right, .. }
5261                        | HydroNode::Join { left, right, .. }) = node
5262                        else {
5263                            unreachable!()
5264                        };
5265
5266                        let is_top_level = left.metadata().location_id.is_top_level()
5267                            && right.metadata().location_id.is_top_level();
5268                        let left_top_level = left.metadata().location_id.is_top_level();
5269                        let right_top_level = right.metadata().location_id.is_top_level();
5270
5271                        let right_ident = ident_stack.pop().unwrap();
5272                        let left_ident = ident_stack.pop().unwrap();
5273
5274                        let stmt_id = next_stmt_id.get_and_increment();
5275                        let stream_ident =
5276                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5277
5278                        match builders_or_callback {
5279                            BuildersOrCallback::Builders(graph_builders) => {
5280                                let left_lifetime = if left_top_level {
5281                                    graph_builders.cross_tick_state_lifetime(&out_location)
5282                                } else {
5283                                    graph_builders.tick_state_lifetime(&out_location)
5284                                };
5285
5286                                let right_lifetime = if right_top_level {
5287                                    graph_builders.cross_tick_state_lifetime(&out_location)
5288                                } else {
5289                                    graph_builders.tick_state_lifetime(&out_location)
5290                                };
5291
5292                                graph_builders.add_dfir_at(
5293                                    &out_location,
5294                                    if is_top_level {
5295                                        // if both inputs are root, the output is expected to have streamy semantics, so we need
5296                                        // a multiset_delta() to negate the replay behavior
5297                                        parse_quote! {
5298                                            #stream_ident = #operator::<#left_lifetime, #right_lifetime>() -> multiset_delta();
5299                                            #left_ident -> [0]#stream_ident;
5300                                            #right_ident -> [1]#stream_ident;
5301                                        }
5302                                    } else {
5303                                        parse_quote! {
5304                                            #stream_ident = #operator::<#left_lifetime, #right_lifetime>();
5305                                            #left_ident -> [0]#stream_ident;
5306                                            #right_ident -> [1]#stream_ident;
5307                                        }
5308                                    },
5309                                    Some(&stmt_id.to_string()),
5310                                );
5311                            }
5312                            BuildersOrCallback::Callback(_, node_callback) => {
5313                                node_callback(node, next_stmt_id);
5314                            }
5315                        }
5316
5317                        ident_stack.push(stream_ident);
5318                    }
5319
5320                    HydroNode::Difference { .. } | HydroNode::AntiJoin { .. } => {
5321                        let operator: syn::Ident = if matches!(node, HydroNode::Difference { .. }) {
5322                            parse_quote!(difference)
5323                        } else {
5324                            parse_quote!(anti_join)
5325                        };
5326
5327                        let (HydroNode::Difference { neg, .. } | HydroNode::AntiJoin { neg, .. }) =
5328                            node
5329                        else {
5330                            unreachable!()
5331                        };
5332
5333                        let neg_top_level = neg.metadata().location_id.is_top_level();
5334
5335                        let neg_ident = ident_stack.pop().unwrap();
5336                        let pos_ident = ident_stack.pop().unwrap();
5337
5338                        let stmt_id = next_stmt_id.get_and_increment();
5339                        let stream_ident =
5340                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5341
5342                        match builders_or_callback {
5343                            BuildersOrCallback::Builders(graph_builders) => {
5344                                let neg_lifetime = if neg_top_level {
5345                                    graph_builders.cross_tick_state_lifetime(&out_location)
5346                                } else {
5347                                    graph_builders.tick_state_lifetime(&out_location)
5348                                };
5349                                let pos_lifetime =
5350                                    graph_builders.tick_state_lifetime(&out_location);
5351
5352                                graph_builders.add_dfir_at(
5353                                    &out_location,
5354                                    parse_quote! {
5355                                        #stream_ident = #operator::<#pos_lifetime, #neg_lifetime>();
5356                                        #pos_ident -> [pos]#stream_ident;
5357                                        #neg_ident -> [neg]#stream_ident;
5358                                    },
5359                                    Some(&stmt_id.to_string()),
5360                                );
5361                            }
5362                            BuildersOrCallback::Callback(_, node_callback) => {
5363                                node_callback(node, next_stmt_id);
5364                            }
5365                        }
5366
5367                        ident_stack.push(stream_ident);
5368                    }
5369
5370                    HydroNode::JoinHalf { .. } => {
5371                        let HydroNode::JoinHalf { right, .. } = node else {
5372                            unreachable!()
5373                        };
5374
5375                        assert!(
5376                            right.metadata().collection_kind.is_bounded(),
5377                            "JoinHalf requires the right (build) side to be Bounded, got {:?}",
5378                            right.metadata().collection_kind
5379                        );
5380
5381                        let build_top_level = right.metadata().location_id.is_top_level();
5382
5383                        let build_ident = ident_stack.pop().unwrap();
5384                        let probe_ident = ident_stack.pop().unwrap();
5385
5386                        let stmt_id = next_stmt_id.get_and_increment();
5387                        let stream_ident =
5388                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5389
5390                        match builders_or_callback {
5391                            BuildersOrCallback::Builders(graph_builders) => {
5392                                let build_lifetime = if build_top_level {
5393                                    graph_builders.cross_tick_state_lifetime(&out_location)
5394                                } else {
5395                                    graph_builders.tick_state_lifetime(&out_location)
5396                                };
5397                                let probe_lifetime =
5398                                    graph_builders.tick_state_lifetime(&out_location);
5399
5400                                graph_builders.add_dfir_at(
5401                                    &out_location,
5402                                    parse_quote! {
5403                                        #stream_ident = join_multiset_half::<#build_lifetime, #probe_lifetime>();
5404                                        #probe_ident -> [probe]#stream_ident;
5405                                        #build_ident -> [build]#stream_ident;
5406                                    },
5407                                    Some(&stmt_id.to_string()),
5408                                );
5409                            }
5410                            BuildersOrCallback::Callback(_, node_callback) => {
5411                                node_callback(node, next_stmt_id);
5412                            }
5413                        }
5414
5415                        ident_stack.push(stream_ident);
5416                    }
5417
5418                    HydroNode::ResolveFutures { .. } => {
5419                        let input_ident = ident_stack.pop().unwrap();
5420
5421                        let stmt_id = next_stmt_id.get_and_increment();
5422                        let futures_ident =
5423                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5424
5425                        match builders_or_callback {
5426                            BuildersOrCallback::Builders(graph_builders) => {
5427                                graph_builders.add_dfir_at(
5428                                    &out_location,
5429                                    parse_quote! {
5430                                        #futures_ident = #input_ident -> resolve_futures();
5431                                    },
5432                                    Some(&stmt_id.to_string()),
5433                                );
5434                            }
5435                            BuildersOrCallback::Callback(_, node_callback) => {
5436                                node_callback(node, next_stmt_id);
5437                            }
5438                        }
5439
5440                        ident_stack.push(futures_ident);
5441                    }
5442
5443                    HydroNode::ResolveFuturesBlocking { .. } => {
5444                        let input_ident = ident_stack.pop().unwrap();
5445
5446                        let stmt_id = next_stmt_id.get_and_increment();
5447                        let futures_ident =
5448                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5449
5450                        match builders_or_callback {
5451                            BuildersOrCallback::Builders(graph_builders) => {
5452                                graph_builders.add_dfir_at(
5453                                    &out_location,
5454                                    parse_quote! {
5455                                        #futures_ident = #input_ident -> resolve_futures_blocking();
5456                                    },
5457                                    Some(&stmt_id.to_string()),
5458                                );
5459                            }
5460                            BuildersOrCallback::Callback(_, node_callback) => {
5461                                node_callback(node, next_stmt_id);
5462                            }
5463                        }
5464
5465                        ident_stack.push(futures_ident);
5466                    }
5467
5468                    HydroNode::ResolveFuturesOrdered { .. } => {
5469                        let input_ident = ident_stack.pop().unwrap();
5470
5471                        let stmt_id = next_stmt_id.get_and_increment();
5472                        let futures_ident =
5473                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5474
5475                        match builders_or_callback {
5476                            BuildersOrCallback::Builders(graph_builders) => {
5477                                graph_builders.add_dfir_at(
5478                                    &out_location,
5479                                    parse_quote! {
5480                                        #futures_ident = #input_ident -> resolve_futures_ordered();
5481                                    },
5482                                    Some(&stmt_id.to_string()),
5483                                );
5484                            }
5485                            BuildersOrCallback::Callback(_, node_callback) => {
5486                                node_callback(node, next_stmt_id);
5487                            }
5488                        }
5489
5490                        ident_stack.push(futures_ident);
5491                    }
5492
5493                    HydroNode::Map {
5494                        f,
5495                        input,
5496                        metadata,
5497                    } => {
5498                        // Pop input ident (pushed last by transform_children).
5499                        let input_ident = ident_stack.pop().unwrap();
5500                        let f_tokens = f.emit_tokens(&mut ident_stack);
5501
5502                        let input_ident = maybe_observe_for_mut(
5503                            f,
5504                            input_ident,
5505                            &input.metadata().location_id,
5506                            &input.metadata().collection_kind,
5507                            &metadata.op,
5508                            builders_or_callback,
5509                            next_stmt_id,
5510                        );
5511
5512                        let stmt_id = next_stmt_id.get_and_increment();
5513                        let map_ident =
5514                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5515
5516                        // A map over a tombstoned update feed transforms the *values* of
5517                        // updates, passing tombstones (`None`) through unchanged. For keyed
5518                        // feeds the inner closure transforms plain `(K, V)` entries and
5519                        // preserves the key (guaranteed by the `KeyedSingleton::map` /
5520                        // `map_with_key` construction), so a tombstoned key passes through
5521                        // with its key intact.
5522                        let map_op: TokenStream = match (
5523                            update_feed_repr(input.metadata()),
5524                            update_feed_repr(metadata),
5525                        ) {
5526                            (UpdateFeedRepr::Plain, UpdateFeedRepr::Plain) => {
5527                                quote! { map(#f_tokens) }
5528                            }
5529                            (UpdateFeedRepr::Tombstoned, UpdateFeedRepr::Tombstoned) => {
5530                                match &metadata.collection_kind {
5531                                    CollectionKind::Optional { .. } => {
5532                                        quote! {
5533                                            map({
5534                                                let mut __hydro_map_f = #f_tokens;
5535                                                move |__hydro_update| __hydro_update.map(&mut __hydro_map_f)
5536                                            })
5537                                        }
5538                                    }
5539                                    CollectionKind::KeyedSingleton { .. } => {
5540                                        quote! {
5541                                            map({
5542                                                let mut __hydro_map_f = #f_tokens;
5543                                                move |(__hydro_key, __hydro_update)| match __hydro_update {
5544                                                    ::std::option::Option::Some(__hydro_value) => {
5545                                                        let (__hydro_out_key, __hydro_out_value) =
5546                                                            (__hydro_map_f)((__hydro_key, __hydro_value));
5547                                                        (__hydro_out_key, ::std::option::Option::Some(__hydro_out_value))
5548                                                    }
5549                                                    ::std::option::Option::None => {
5550                                                        (__hydro_key, ::std::option::Option::None)
5551                                                    }
5552                                                }
5553                                            })
5554                                        }
5555                                    }
5556                                    other => panic!(
5557                                        "unexpected tombstoned update feed kind for `map`: {:?}",
5558                                        other
5559                                    ),
5560                                }
5561                            }
5562                            (in_repr, out_repr) => panic!(
5563                                "map may not change the update feed representation ({:?} -> {:?})",
5564                                in_repr, out_repr
5565                            ),
5566                        };
5567
5568                        match builders_or_callback {
5569                            BuildersOrCallback::Builders(graph_builders) => {
5570                                graph_builders.add_dfir_at(
5571                                    &out_location,
5572                                    parse_quote! {
5573                                        #map_ident = #input_ident -> #map_op;
5574                                    },
5575                                    Some(&stmt_id.to_string()),
5576                                );
5577                            }
5578                            BuildersOrCallback::Callback(_, node_callback) => {
5579                                node_callback(node, next_stmt_id);
5580                            }
5581                        }
5582
5583                        ident_stack.push(map_ident);
5584                    }
5585
5586                    HydroNode::FlatMap { f, input, metadata } => {
5587                        let input_ident = ident_stack.pop().unwrap();
5588                        let f_tokens = f.emit_tokens(&mut ident_stack);
5589
5590                        let input_ident = maybe_observe_for_mut(
5591                            f, input_ident,
5592                            &input.metadata().location_id,
5593                            &input.metadata().collection_kind,
5594                            &metadata.op,
5595                            builders_or_callback, next_stmt_id,
5596                        );
5597
5598                        let stmt_id = next_stmt_id.get_and_increment();
5599                        let flat_map_ident =
5600                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5601
5602                        match builders_or_callback {
5603                            BuildersOrCallback::Builders(graph_builders) => {
5604                                graph_builders.add_dfir_at(
5605                                    &out_location,
5606                                    parse_quote! {
5607                                        #flat_map_ident = #input_ident -> flat_map(#f_tokens);
5608                                    },
5609                                    Some(&stmt_id.to_string()),
5610                                );
5611                            }
5612                            BuildersOrCallback::Callback(_, node_callback) => {
5613                                node_callback(node, next_stmt_id);
5614                            }
5615                        }
5616
5617                        ident_stack.push(flat_map_ident);
5618                    }
5619
5620                    HydroNode::FlatMapStreamBlocking { f, input, metadata } => {
5621                        let input_ident = ident_stack.pop().unwrap();
5622                        let f_tokens = f.emit_tokens(&mut ident_stack);
5623
5624                        let input_ident = maybe_observe_for_mut(
5625                            f, input_ident,
5626                            &input.metadata().location_id,
5627                            &input.metadata().collection_kind,
5628                            &metadata.op,
5629                            builders_or_callback, next_stmt_id,
5630                        );
5631
5632                        let stmt_id = next_stmt_id.get_and_increment();
5633                        let flat_map_stream_blocking_ident =
5634                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5635
5636                        match builders_or_callback {
5637                            BuildersOrCallback::Builders(graph_builders) => {
5638                                graph_builders.add_dfir_at(
5639                                    &out_location,
5640                                    parse_quote! {
5641                                        #flat_map_stream_blocking_ident = #input_ident -> flat_map_stream_blocking(#f_tokens);
5642                                    },
5643                                    Some(&stmt_id.to_string()),
5644                                );
5645                            }
5646                            BuildersOrCallback::Callback(_, node_callback) => {
5647                                node_callback(node, next_stmt_id);
5648                            }
5649                        }
5650
5651                        ident_stack.push(flat_map_stream_blocking_ident);
5652                    }
5653
5654                    HydroNode::Filter { f, input, metadata } => {
5655                        let input_ident = ident_stack.pop().unwrap();
5656                        let f_tokens = f.emit_tokens(&mut ident_stack);
5657
5658                        let input_ident = maybe_observe_for_mut(
5659                            f, input_ident,
5660                            &input.metadata().location_id,
5661                            &input.metadata().collection_kind,
5662                            &metadata.op,
5663                            builders_or_callback, next_stmt_id,
5664                        );
5665
5666                        let stmt_id = next_stmt_id.get_and_increment();
5667                        let filter_ident =
5668                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5669
5670                        // Filtering *state* (as opposed to events) makes absence explicit:
5671                        // - On a tombstoned update feed, each update's value is filtered, and a
5672                        //   filtered-out value becomes a tombstone — dropping the update
5673                        //   entirely would incorrectly leave the previous value observable.
5674                        // - A filter that takes a plain feed *into* a tombstoned feed (e.g.
5675                        //   `Singleton::filter` / `Optional::<InitNone>::filter` at the root,
5676                        //   which produce root unbounded optionals) emits an update for every
5677                        //   input value: `Some(v)` if it passes, a `None` tombstone if not.
5678                        let filter_op: TokenStream = match (
5679                            update_feed_repr(input.metadata()),
5680                            update_feed_repr(metadata),
5681                        ) {
5682                            (UpdateFeedRepr::Plain, UpdateFeedRepr::Plain) => {
5683                                quote! { filter(#f_tokens) }
5684                            }
5685                            (UpdateFeedRepr::Plain, UpdateFeedRepr::Tombstoned) => {
5686                                match &metadata.collection_kind {
5687                                    CollectionKind::Optional { .. } => {
5688                                        quote! {
5689                                            map({
5690                                                let mut __hydro_filter_f = #f_tokens;
5691                                                move |__hydro_value| {
5692                                                    if (__hydro_filter_f)(&__hydro_value) {
5693                                                        Some(__hydro_value)
5694                                                    } else {
5695                                                        None
5696                                                    }
5697                                                }
5698                                            })
5699                                        }
5700                                    }
5701                                    // `KeyedSingleton::filter` preserves the bound (`B -> B`),
5702                                    // so a plain keyed feed can never filter into a tombstoned
5703                                    // one.
5704                                    other => panic!(
5705                                        "unexpected tombstoned update feed kind for `filter`: {:?}",
5706                                        other
5707                                    ),
5708                                }
5709                            }
5710                            (UpdateFeedRepr::Tombstoned, UpdateFeedRepr::Tombstoned) => {
5711                                match &metadata.collection_kind {
5712                                    CollectionKind::Optional { .. } => {
5713                                        quote! {
5714                                            map({
5715                                                let mut __hydro_filter_f = #f_tokens;
5716                                                move |__hydro_update: Option<_>| {
5717                                                    __hydro_update.filter(|__hydro_value| (__hydro_filter_f)(__hydro_value))
5718                                                }
5719                                            })
5720                                        }
5721                                    }
5722                                    CollectionKind::KeyedSingleton { .. } => {
5723                                        // The inner closure filters plain `(K, V)` entries by
5724                                        // reference; a filtered-out entry tombstones its key.
5725                                        quote! {
5726                                            map({
5727                                                let mut __hydro_filter_f = #f_tokens;
5728                                                move |(__hydro_key, __hydro_update)| match __hydro_update {
5729                                                    ::std::option::Option::Some(__hydro_value) => {
5730                                                        let __hydro_entry = (__hydro_key, __hydro_value);
5731                                                        if (__hydro_filter_f)(&__hydro_entry) {
5732                                                            let (__hydro_key, __hydro_value) = __hydro_entry;
5733                                                            (__hydro_key, ::std::option::Option::Some(__hydro_value))
5734                                                        } else {
5735                                                            let (__hydro_key, _) = __hydro_entry;
5736                                                            (__hydro_key, ::std::option::Option::None)
5737                                                        }
5738                                                    }
5739                                                    ::std::option::Option::None => {
5740                                                        (__hydro_key, ::std::option::Option::None)
5741                                                    }
5742                                                }
5743                                            })
5744                                        }
5745                                    }
5746                                    other => panic!(
5747                                        "unexpected tombstoned update feed kind for `filter`: {:?}",
5748                                        other
5749                                    ),
5750                                }
5751                            }
5752                            (in_repr, out_repr) => panic!(
5753                                "filter may not un-tombstone an update feed ({:?} -> {:?})",
5754                                in_repr, out_repr
5755                            ),
5756                        };
5757
5758                        match builders_or_callback {
5759                            BuildersOrCallback::Builders(graph_builders) => {
5760                                graph_builders.add_dfir_at(
5761                                    &out_location,
5762                                    parse_quote! {
5763                                        #filter_ident = #input_ident -> #filter_op;
5764                                    },
5765                                    Some(&stmt_id.to_string()),
5766                                );
5767                            }
5768                            BuildersOrCallback::Callback(_, node_callback) => {
5769                                node_callback(node, next_stmt_id);
5770                            }
5771                        }
5772
5773                        ident_stack.push(filter_ident);
5774                    }
5775
5776                    HydroNode::FilterMap { f, input, metadata } => {
5777                        let input_ident = ident_stack.pop().unwrap();
5778                        let f_tokens = f.emit_tokens(&mut ident_stack);
5779
5780                        let input_ident = maybe_observe_for_mut(
5781                            f, input_ident,
5782                            &input.metadata().location_id,
5783                            &input.metadata().collection_kind,
5784                            &metadata.op,
5785                            builders_or_callback, next_stmt_id,
5786                        );
5787
5788                        let stmt_id = next_stmt_id.get_and_increment();
5789                        let filter_map_ident =
5790                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5791
5792                        // See the `Filter` arm: filtering state makes absence explicit.
5793                        // - Plain -> tombstoned (e.g. `Singleton::filter_map` /
5794                        //   `Optional::<InitNone>::filter_map` at the root): `f` already returns
5795                        //   `Option<U>`, which is exactly the update record: `map(f)`.
5796                        // - Tombstoned -> tombstoned: apply `f` to the value of each update;
5797                        //   both "was a tombstone" and "filtered out" yield a tombstone. For
5798                        //   keyed feeds the inner closure consumes the `(K, V)` entry, so the
5799                        //   key is cloned up front to tombstone filtered-out entries
5800                        //   (generated code requires `K: Clone`).
5801                        let filter_map_op: TokenStream = match (
5802                            update_feed_repr(input.metadata()),
5803                            update_feed_repr(metadata),
5804                        ) {
5805                            (UpdateFeedRepr::Plain, UpdateFeedRepr::Plain) => {
5806                                quote! { filter_map(#f_tokens) }
5807                            }
5808                            (UpdateFeedRepr::Plain, UpdateFeedRepr::Tombstoned) => {
5809                                match &metadata.collection_kind {
5810                                    CollectionKind::Optional { .. } => {
5811                                        quote! { map(#f_tokens) }
5812                                    }
5813                                    // `KeyedSingleton::filter_map` erases monotonicity but
5814                                    // never *introduces* `Unbounded` (`EraseMonotonic` of the
5815                                    // plain keyed kinds is itself plain), so a plain keyed
5816                                    // feed can never filter-map into a tombstoned one.
5817                                    other => panic!(
5818                                        "unexpected tombstoned update feed kind for `filter_map`: {:?}",
5819                                        other
5820                                    ),
5821                                }
5822                            }
5823                            (UpdateFeedRepr::Tombstoned, UpdateFeedRepr::Tombstoned) => {
5824                                match &metadata.collection_kind {
5825                                    CollectionKind::Optional { .. } => {
5826                                        quote! {
5827                                            map({
5828                                                let mut __hydro_filter_map_f = #f_tokens;
5829                                                move |__hydro_update| {
5830                                                    __hydro_update.and_then(&mut __hydro_filter_map_f)
5831                                                }
5832                                            })
5833                                        }
5834                                    }
5835                                    CollectionKind::KeyedSingleton { .. } => {
5836                                        quote! {
5837                                            map({
5838                                                let mut __hydro_filter_map_f = #f_tokens;
5839                                                move |(__hydro_key, __hydro_update)| match __hydro_update {
5840                                                    ::std::option::Option::Some(__hydro_value) => {
5841                                                        let __hydro_key_backup =
5842                                                            ::std::clone::Clone::clone(&__hydro_key);
5843                                                        match (__hydro_filter_map_f)((__hydro_key, __hydro_value)) {
5844                                                            ::std::option::Option::Some((__hydro_out_key, __hydro_out_value)) => {
5845                                                                (__hydro_out_key, ::std::option::Option::Some(__hydro_out_value))
5846                                                            }
5847                                                            ::std::option::Option::None => {
5848                                                                (__hydro_key_backup, ::std::option::Option::None)
5849                                                            }
5850                                                        }
5851                                                    }
5852                                                    ::std::option::Option::None => {
5853                                                        (__hydro_key, ::std::option::Option::None)
5854                                                    }
5855                                                }
5856                                            })
5857                                        }
5858                                    }
5859                                    other => panic!(
5860                                        "unexpected tombstoned update feed kind for `filter_map`: {:?}",
5861                                        other
5862                                    ),
5863                                }
5864                            }
5865                            (in_repr, out_repr) => panic!(
5866                                "filter_map may not un-tombstone an update feed ({:?} -> {:?})",
5867                                in_repr, out_repr
5868                            ),
5869                        };
5870
5871                        match builders_or_callback {
5872                            BuildersOrCallback::Builders(graph_builders) => {
5873                                graph_builders.add_dfir_at(
5874                                    &out_location,
5875                                    parse_quote! {
5876                                        #filter_map_ident = #input_ident -> #filter_map_op;
5877                                    },
5878                                    Some(&stmt_id.to_string()),
5879                                );
5880                            }
5881                            BuildersOrCallback::Callback(_, node_callback) => {
5882                                node_callback(node, next_stmt_id);
5883                            }
5884                        }
5885
5886                        ident_stack.push(filter_map_ident);
5887                    }
5888
5889                    HydroNode::Sort { .. } => {
5890                        let input_ident = ident_stack.pop().unwrap();
5891
5892                        let stmt_id = next_stmt_id.get_and_increment();
5893                        let sort_ident =
5894                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5895
5896                        match builders_or_callback {
5897                            BuildersOrCallback::Builders(graph_builders) => {
5898                                graph_builders.add_dfir_at(
5899                                    &out_location,
5900                                    parse_quote! {
5901                                        #sort_ident = #input_ident -> sort();
5902                                    },
5903                                    Some(&stmt_id.to_string()),
5904                                );
5905                            }
5906                            BuildersOrCallback::Callback(_, node_callback) => {
5907                                node_callback(node, next_stmt_id);
5908                            }
5909                        }
5910
5911                        ident_stack.push(sort_ident);
5912                    }
5913
5914                    HydroNode::DeferTick { .. } => {
5915                        let input_ident = ident_stack.pop().unwrap();
5916
5917                        let stmt_id = next_stmt_id.get_and_increment();
5918                        let defer_tick_ident =
5919                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5920
5921                        match builders_or_callback {
5922                            BuildersOrCallback::Builders(graph_builders) => {
5923                                graph_builders.add_dfir_at(
5924                                    &out_location,
5925                                    parse_quote! {
5926                                        #defer_tick_ident = #input_ident -> defer_tick_lazy();
5927                                    },
5928                                    Some(&stmt_id.to_string()),
5929                                );
5930                            }
5931                            BuildersOrCallback::Callback(_, node_callback) => {
5932                                node_callback(node, next_stmt_id);
5933                            }
5934                        }
5935
5936                        ident_stack.push(defer_tick_ident);
5937                    }
5938
5939                    HydroNode::Enumerate { input, .. } => {
5940                        let input_ident = ident_stack.pop().unwrap();
5941
5942                        let stmt_id = next_stmt_id.get_and_increment();
5943                        let enumerate_ident =
5944                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5945
5946                        match builders_or_callback {
5947                            BuildersOrCallback::Builders(graph_builders) => {
5948                                let lifetime = if input.metadata().location_id.is_top_level() {
5949                                    graph_builders.cross_tick_state_lifetime(&out_location)
5950                                } else {
5951                                    graph_builders.tick_state_lifetime(&out_location)
5952                                };
5953                                graph_builders.add_dfir_at(
5954                                    &out_location,
5955                                    parse_quote! {
5956                                        #enumerate_ident = #input_ident -> enumerate::<#lifetime>();
5957                                    },
5958                                    Some(&stmt_id.to_string()),
5959                                );
5960                            }
5961                            BuildersOrCallback::Callback(_, node_callback) => {
5962                                node_callback(node, next_stmt_id);
5963                            }
5964                        }
5965
5966                        ident_stack.push(enumerate_ident);
5967                    }
5968
5969                    HydroNode::Inspect { f, input, metadata } => {
5970                        let input_ident = ident_stack.pop().unwrap();
5971                        let f_tokens = f.emit_tokens(&mut ident_stack);
5972
5973                        let input_ident = maybe_observe_for_mut(
5974                            f, input_ident,
5975                            &input.metadata().location_id,
5976                            &input.metadata().collection_kind,
5977                            &metadata.op,
5978                            builders_or_callback, next_stmt_id,
5979                        );
5980
5981                        let stmt_id = next_stmt_id.get_and_increment();
5982                        let inspect_ident =
5983                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
5984
5985                        // A tombstoned update feed (root unbounded Optional/KeyedSingleton)
5986                        // cannot reach this arm today: no `Optional`/`Singleton` API
5987                        // constructs `Inspect` nodes, and `KeyedSingleton::inspect` /
5988                        // `inspect_with_key` are only available for `ValueBound = Bounded`
5989                        // kinds (`BoundedValue`/`Bounded`), which keep the plain entry
5990                        // representation. Guard against silently inspecting raw update
5991                        // records if a future API change makes this reachable.
5992                        if update_feed_repr(input.metadata()) == UpdateFeedRepr::Tombstoned
5993                            || update_feed_repr(metadata) == UpdateFeedRepr::Tombstoned
5994                        {
5995                            todo!(
5996                                "`inspect` on a tombstoned update feed (root unbounded \
5997                                 Optional/KeyedSingleton) is not yet supported"
5998                            );
5999                        }
6000
6001                        match builders_or_callback {
6002                            BuildersOrCallback::Builders(graph_builders) => {
6003                                graph_builders.add_dfir_at(
6004                                    &out_location,
6005                                    parse_quote! {
6006                                        #inspect_ident = #input_ident -> inspect(#f_tokens);
6007                                    },
6008                                    Some(&stmt_id.to_string()),
6009                                );
6010                            }
6011                            BuildersOrCallback::Callback(_, node_callback) => {
6012                                node_callback(node, next_stmt_id);
6013                            }
6014                        }
6015
6016                        ident_stack.push(inspect_ident);
6017                    }
6018
6019                    HydroNode::Unique { input, .. } => {
6020                        let input_ident = ident_stack.pop().unwrap();
6021
6022                        let stmt_id = next_stmt_id.get_and_increment();
6023                        let unique_ident =
6024                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
6025
6026                        match builders_or_callback {
6027                            BuildersOrCallback::Builders(graph_builders) => {
6028                                let lifetime = if input.metadata().location_id.is_top_level() {
6029                                    graph_builders.cross_tick_state_lifetime(&out_location)
6030                                } else {
6031                                    graph_builders.tick_state_lifetime(&out_location)
6032                                };
6033
6034                                graph_builders.add_dfir_at(
6035                                    &out_location,
6036                                    parse_quote! {
6037                                        #unique_ident = #input_ident -> unique::<#lifetime>();
6038                                    },
6039                                    Some(&stmt_id.to_string()),
6040                                );
6041                            }
6042                            BuildersOrCallback::Callback(_, node_callback) => {
6043                                node_callback(node, next_stmt_id);
6044                            }
6045                        }
6046
6047                        ident_stack.push(unique_ident);
6048                    }
6049
6050                    HydroNode::Fold { .. } | HydroNode::FoldKeyed { .. } | HydroNode::Scan { .. } | HydroNode::ScanAsyncBlocking { .. } => {
6051                        let operator: syn::Ident = if let HydroNode::Fold { input, .. } = node {
6052                            if input.metadata().location_id.is_root()
6053                                && input.metadata().collection_kind.is_bounded()
6054                            {
6055                                parse_quote!(fold_no_replay)
6056                            } else {
6057                                parse_quote!(fold)
6058                            }
6059                        } else if matches!(node, HydroNode::Scan { .. }) {
6060                            parse_quote!(scan)
6061                        } else if matches!(node, HydroNode::ScanAsyncBlocking { .. }) {
6062                            parse_quote!(scan_async_blocking)
6063                        } else if let HydroNode::FoldKeyed { input, .. } = node {
6064                            if input.metadata().location_id.is_root()
6065                                && input.metadata().collection_kind.is_bounded()
6066                            {
6067                                todo!("Fold keyed on a top-level bounded collection is not yet supported")
6068                            } else {
6069                                parse_quote!(fold_keyed)
6070                            }
6071                        } else {
6072                            unreachable!()
6073                        };
6074
6075                        let (HydroNode::Fold { input, .. }
6076                        | HydroNode::FoldKeyed { input, .. }
6077                        | HydroNode::Scan { input, .. }
6078                        | HydroNode::ScanAsyncBlocking { input, .. }) = node
6079                        else {
6080                            unreachable!()
6081                        };
6082
6083                        let input_top_level = input.metadata().location_id.is_top_level();
6084
6085                        let input_ident = ident_stack.pop().unwrap();
6086
6087                        let (HydroNode::Fold { init, acc, .. }
6088                        | HydroNode::FoldKeyed { init, acc, .. }
6089                        | HydroNode::Scan { init, acc, .. }
6090                        | HydroNode::ScanAsyncBlocking { init, acc, .. }) = &*node
6091                        else {
6092                            unreachable!()
6093                        };
6094
6095                        let acc_tokens = acc.emit_tokens(&mut ident_stack);
6096                        let init_tokens = init.emit_tokens(&mut ident_stack);
6097
6098                        let stmt_id = next_stmt_id.get_and_increment();
6099                        let fold_ident =
6100                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
6101
6102                        match builders_or_callback {
6103                            BuildersOrCallback::Builders(graph_builders) => {
6104                                let lifetime = if input_top_level {
6105                                    graph_builders.cross_tick_state_lifetime(&out_location)
6106                                } else {
6107                                    graph_builders.tick_state_lifetime(&out_location)
6108                                };
6109
6110                                if matches!(node, HydroNode::Fold { .. })
6111                                    && node.metadata().location_id.is_root()
6112                                    && graph_builders.singleton_intermediates()
6113                                    && !node.metadata().collection_kind.is_bounded()
6114                                {
6115                                    let HydroNode::Fold { input, .. } = &*node else { unreachable!() };
6116                                    let hooked_input_ident = graph_builders.emit_fold_hook(
6117                                        &input.metadata().location_id,
6118                                        &input_ident,
6119                                        &input.metadata().collection_kind,
6120                                        &node.metadata().op,
6121                                    );
6122
6123                                    let (effective_input, wrapped_acc) = if let Some(ref hooked) = hooked_input_ident {
6124                                        let acc: syn::Expr = parse_quote!({
6125                                            let mut __inner = #acc_tokens;
6126                                            move |__state, __batch: Vec<_>| {
6127                                                if __batch.is_empty() {
6128                                                    return None;
6129                                                }
6130                                                for __value in __batch {
6131                                                    __inner(__state, __value);
6132                                                }
6133                                                Some(__state.clone())
6134                                            }
6135                                        });
6136                                        (hooked, acc)
6137                                    } else {
6138                                        let acc: syn::Expr = parse_quote!({
6139                                            let mut __inner = #acc_tokens;
6140                                            move |__state, __value| {
6141                                                __inner(__state, __value);
6142                                                Some(__state.clone())
6143                                            }
6144                                        });
6145                                        (&input_ident, acc)
6146                                    };
6147
6148                                    graph_builders.add_dfir_at(
6149                                        &out_location,
6150                                        parse_quote! {
6151                                            source_iter([(#init_tokens)()]) -> [0]#fold_ident;
6152                                            #effective_input -> scan::<#lifetime>(#init_tokens, #wrapped_acc) -> [1]#fold_ident;
6153                                            #fold_ident = chain();
6154                                        },
6155                                        Some(&stmt_id.to_string()),
6156                                    );
6157
6158                                    // A *scripted* fold releases exactly one element per
6159                                    // decision, producing one new version per release, so
6160                                    // its snapshot takes the ordinary (scriptable) path
6161                                    // rather than the fuzz-passthrough shortcut.
6162                                    if hooked_input_ident.is_some()
6163                                        && node.metadata().op.sim_hook_id.is_none()
6164                                    {
6165                                        fold_hooked_idents.insert(fold_ident.to_string());
6166                                    }
6167                                } else if matches!(node, HydroNode::FoldKeyed { .. })
6168                                    && node.metadata().location_id.is_root()
6169                                    && graph_builders.singleton_intermediates()
6170                                    && !node.metadata().collection_kind.is_bounded()
6171                                {
6172                                    let HydroNode::FoldKeyed { input, .. } = &*node else { unreachable!() };
6173                                    let hooked_input_ident = graph_builders.emit_fold_hook(
6174                                        &input.metadata().location_id,
6175                                        &input_ident,
6176                                        &input.metadata().collection_kind,
6177                                        &node.metadata().op,
6178                                    );
6179
6180                                    let wrapped_acc: syn::Expr = parse_quote!({
6181                                        let mut __init = #init_tokens;
6182                                        let mut __inner = #acc_tokens;
6183                                        move |__state, __kv: (_, _)| {
6184                                            // TODO(shadaj): we can avoid the clone when the entry exists
6185                                            let __state = __state
6186                                                .entry(::std::clone::Clone::clone(&__kv.0))
6187                                                .or_insert_with(|| (__init)());
6188                                            __inner(__state, __kv.1);
6189                                            Some((__kv.0, ::std::clone::Clone::clone(&*__state)))
6190                                        }
6191                                    });
6192
6193                                    if let Some(hooked_input_ident) = hooked_input_ident {
6194                                        graph_builders.add_dfir_at(
6195                                            &out_location,
6196                                            parse_quote! {
6197                                                #fold_ident = #hooked_input_ident -> flatten() -> scan::<#lifetime>(|| ::std::collections::HashMap::new(), #wrapped_acc);
6198                                            },
6199                                            Some(&stmt_id.to_string()),
6200                                        );
6201
6202                                        fold_hooked_idents.insert(fold_ident.to_string());
6203                                    } else {
6204                                        graph_builders.add_dfir_at(
6205                                            &out_location,
6206                                            parse_quote! {
6207                                                #fold_ident = #input_ident -> scan::<#lifetime>(|| ::std::collections::HashMap::new(), #wrapped_acc);
6208                                            },
6209                                            Some(&stmt_id.to_string()),
6210                                        );
6211                                    }
6212                                } else if (matches!(node, HydroNode::Fold { .. })
6213                                    || matches!(node, HydroNode::FoldKeyed { .. }))
6214                                    && !node.metadata().location_id.is_top_level()
6215                                    && graph_builders.singleton_intermediates()
6216                                {
6217                                    let input_ref = match &*node {
6218                                        HydroNode::Fold { input, .. } => input,
6219                                        HydroNode::FoldKeyed { input, .. } => input,
6220                                        _ => unreachable!(),
6221                                    };
6222                                    let hooked_input_ident = graph_builders.emit_fold_hook(
6223                                        &input_ref.metadata().location_id,
6224                                        &input_ident,
6225                                        &input_ref.metadata().collection_kind,
6226                                        &node.metadata().op,
6227                                    );
6228
6229                                    let actual_input = hooked_input_ident.as_ref().unwrap_or(&input_ident);
6230                                    graph_builders.add_dfir_at(
6231                                        &out_location,
6232                                        parse_quote! {
6233                                            #fold_ident = #actual_input -> #operator::<#lifetime>(#init_tokens, #acc_tokens);
6234                                        },
6235                                        Some(&stmt_id.to_string()),
6236                                    );
6237                                } else {
6238                                    // No `fold`-family API currently produces a root unbounded
6239                                    // (tombstoned) keyed singleton (`KeyedStream::fold` yields
6240                                    // monotone keyed bounds), so there are no update records to
6241                                    // wrap here; fail loudly if that ever changes rather than
6242                                    // emitting plain entries onto a tombstoned feed.
6243                                    assert_eq!(
6244                                        update_feed_repr(node.metadata()),
6245                                        UpdateFeedRepr::Plain,
6246                                        "`fold`-family aggregation into a tombstoned update feed \
6247                                         (root unbounded Optional/KeyedSingleton) is not supported"
6248                                    );
6249                                    graph_builders.add_dfir_at(
6250                                        &out_location,
6251                                        parse_quote! {
6252                                            #fold_ident = #input_ident -> #operator::<#lifetime>(#init_tokens, #acc_tokens);
6253                                        },
6254                                        Some(&stmt_id.to_string()),
6255                                    );
6256                                }
6257                            }
6258                            BuildersOrCallback::Callback(_, node_callback) => {
6259                                node_callback(node, next_stmt_id);
6260                            }
6261                        }
6262
6263                        ident_stack.push(fold_ident);
6264                    }
6265
6266                    HydroNode::Reduce { .. } | HydroNode::ReduceKeyed { .. } => {
6267                        let operator: syn::Ident = if let HydroNode::Reduce { input, .. } = node {
6268                            if input.metadata().location_id.is_root()
6269                                && input.metadata().collection_kind.is_bounded()
6270                            {
6271                                parse_quote!(reduce_no_replay)
6272                            } else {
6273                                parse_quote!(reduce)
6274                            }
6275                        } else if let HydroNode::ReduceKeyed { input, .. } = node {
6276                            if input.metadata().location_id.is_root()
6277                                && input.metadata().collection_kind.is_bounded()
6278                            {
6279                                todo!(
6280                                    "Calling keyed reduce on a top-level bounded collection is not supported"
6281                                )
6282                            } else {
6283                                parse_quote!(reduce_keyed)
6284                            }
6285                        } else {
6286                            unreachable!()
6287                        };
6288
6289                        let (HydroNode::Reduce { input, .. } | HydroNode::ReduceKeyed { input, .. }) = node
6290                        else {
6291                            unreachable!()
6292                        };
6293
6294                        let input_top_level = input.metadata().location_id.is_top_level();
6295
6296                        let input_ident = ident_stack.pop().unwrap();
6297
6298                        let (HydroNode::Reduce { f, .. } | HydroNode::ReduceKeyed { f, .. }) = &*node
6299                        else {
6300                            unreachable!()
6301                        };
6302
6303                        let f_tokens = f.emit_tokens(&mut ident_stack);
6304
6305                        let stmt_id = next_stmt_id.get_and_increment();
6306                        let reduce_ident =
6307                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
6308
6309                        match builders_or_callback {
6310                            BuildersOrCallback::Builders(graph_builders) => {
6311                                let lifetime = if input_top_level {
6312                                    graph_builders.cross_tick_state_lifetime(&out_location)
6313                                } else {
6314                                    graph_builders.tick_state_lifetime(&out_location)
6315                                };
6316
6317                                if matches!(node, HydroNode::Reduce { .. })
6318                                    && node.metadata().location_id.is_root()
6319                                    && graph_builders.singleton_intermediates()
6320                                    && !node.metadata().collection_kind.is_bounded()
6321                                {
6322                                    todo!(
6323                                        "Reduce with optional intermediates is not yet supported in simulator"
6324                                    );
6325                                } else if matches!(node, HydroNode::ReduceKeyed { .. })
6326                                    && node.metadata().location_id.is_root()
6327                                    && graph_builders.singleton_intermediates()
6328                                    && !node.metadata().collection_kind.is_bounded()
6329                                {
6330                                    todo!(
6331                                        "Reduce keyed with optional intermediates is not yet supported in simulator"
6332                                    );
6333                                } else {
6334                                    // A root unbounded keyed singleton is a tombstoned
6335                                    // `(K, Option<V>)` update feed: wrap each emitted entry as
6336                                    // an upsert. (`reduce_keyed` never removes keys, so no
6337                                    // tombstones are needed.)
6338                                    let wrap_op: TokenStream = if matches!(
6339                                        node.metadata().collection_kind,
6340                                        CollectionKind::KeyedSingleton { .. }
6341                                    ) && update_feed_repr(node.metadata())
6342                                        == UpdateFeedRepr::Tombstoned
6343                                    {
6344                                        quote! { -> map(|(__hydro_key, __hydro_value)| (__hydro_key, ::std::option::Option::Some(__hydro_value))) }
6345                                    } else {
6346                                        quote! {}
6347                                    };
6348                                    graph_builders.add_dfir_at(
6349                                        &out_location,
6350                                        parse_quote! {
6351                                            #reduce_ident = #input_ident -> #operator::<#lifetime>(#f_tokens) #wrap_op;
6352                                        },
6353                                        Some(&stmt_id.to_string()),
6354                                    );
6355                                }
6356                            }
6357                            BuildersOrCallback::Callback(_, node_callback) => {
6358                                node_callback(node, next_stmt_id);
6359                            }
6360                        }
6361
6362                        ident_stack.push(reduce_ident);
6363                    }
6364
6365                      HydroNode::ReduceKeyedWatermark {
6366                          f,
6367                          input,
6368                          watermark,
6369                          metadata,
6370                      } => {
6371
6372                          // watermark is processed second, so it's on top
6373                          let watermark_ident = ident_stack.pop().unwrap();
6374                          let watermark_location = watermark.metadata().location_id.clone();
6375                          let input_ident = ident_stack.pop().unwrap();
6376                          let f_tokens = f.emit_tokens(&mut ident_stack);
6377
6378                        let stmt_id = next_stmt_id.get_and_increment();
6379                        let chain_ident = syn::Ident::new(
6380                            &format!("reduce_keyed_watermark_chain_{}", stmt_id),
6381                            Span::call_site(),
6382                        );
6383
6384                        let fold_ident =
6385                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
6386
6387                        let agg_operator: syn::Ident = if input.metadata().location_id.is_root()
6388                            && input.metadata().collection_kind.is_bounded()
6389                        {
6390                            parse_quote!(fold_no_replay)
6391                        } else {
6392                            parse_quote!(fold)
6393                        };
6394
6395                          match builders_or_callback {
6396                              BuildersOrCallback::Builders(graph_builders) => {
6397                                  // The watermark lives at its own (tick) location; if that is a
6398                                  // different loop context than where this reduce is emitted
6399                                  // (`out_location`), un-window it so the edge does not illegally
6400                                  // exit the loop.
6401                                  let watermark_ident = graph_builders.unwindow_for_consume(
6402                                      watermark_ident,
6403                                      &watermark_location,
6404                                      &out_location,
6405                                  );
6406
6407                                  let lifetime = if input.metadata().location_id.is_top_level() {
6408                                      graph_builders.cross_tick_state_lifetime(&out_location)
6409                                  } else {
6410                                      graph_builders.tick_state_lifetime(&out_location)
6411                                  };
6412
6413                                if metadata.location_id.is_root()
6414                                    && graph_builders.singleton_intermediates()
6415                                    && !metadata.collection_kind.is_bounded()
6416                                {
6417                                    todo!(
6418                                        "Reduce keyed watermarked on a top-level bounded collection is not yet supported"
6419                                    )
6420                                } else if update_feed_repr(metadata) == UpdateFeedRepr::Tombstoned {
6421                                    // A root unbounded keyed singleton is a tombstoned
6422                                    // `(K, Option<V>)` update feed (see
6423                                    // [`CollectionKind::root_repr_is_tombstoned_update_feed`]).
6424                                    // Instead of re-emitting the whole entry map, emit *deltas*:
6425                                    // each payload that lands in the map yields an upsert
6426                                    // `(k, Some(v))` with the key's new reduced value, and each
6427                                    // key garbage-collected by a watermark advance yields a
6428                                    // tombstone `(k, None)`. Downstream consumers reconstruct
6429                                    // the current map by applying updates (see
6430                                    // [`DfirBuilder::batch`]). Generated code requires
6431                                    // `K: Clone` and `V: Clone`, as the whole-map form below
6432                                    // does too (there the clones are hidden inside the `fold`
6433                                    // operator, which clones its accumulator — the entire map —
6434                                    // each time it emits downstream).
6435                                    graph_builders.add_dfir_at(
6436                                        &out_location,
6437                                        parse_quote! {
6438                                            #chain_ident = chain();
6439                                            #input_ident
6440                                                -> map(|x| (Some(x), None))
6441                                                -> [0]#chain_ident;
6442                                            #watermark_ident
6443                                                -> map(|watermark| (None, Some(watermark)))
6444                                                -> [1]#chain_ident;
6445
6446                                            #fold_ident = #chain_ident
6447                                                -> scan::<#lifetime>(|| (::std::collections::HashMap::new(), None), {
6448                                                    let __reduce_keyed_fn = #f_tokens;
6449                                                    move |(map, opt_curr_watermark), (opt_payload, opt_watermark)| {
6450                                                        let mut deltas = ::std::vec::Vec::new();
6451                                                        if let Some((k, v)) = opt_payload {
6452                                                            if let Some(curr_watermark) = *opt_curr_watermark {
6453                                                                if k < curr_watermark {
6454                                                                    return Some(deltas);
6455                                                                }
6456                                                            }
6457                                                            match map.entry(k) {
6458                                                                ::std::collections::hash_map::Entry::Vacant(e) => {
6459                                                                    let key = ::std::clone::Clone::clone(e.key());
6460                                                                    let value = ::std::clone::Clone::clone(e.insert(v));
6461                                                                    deltas.push((key, ::std::option::Option::Some(value)));
6462                                                                }
6463                                                                ::std::collections::hash_map::Entry::Occupied(mut e) => {
6464                                                                    __reduce_keyed_fn(e.get_mut(), v);
6465                                                                    deltas.push((
6466                                                                        ::std::clone::Clone::clone(e.key()),
6467                                                                        ::std::option::Option::Some(::std::clone::Clone::clone(e.get())),
6468                                                                    ));
6469                                                                }
6470                                                            }
6471                                                        } else {
6472                                                            let watermark = opt_watermark.unwrap();
6473                                                            if let Some(curr_watermark) = *opt_curr_watermark {
6474                                                                if watermark <= curr_watermark {
6475                                                                    return Some(deltas);
6476                                                                }
6477                                                            }
6478                                                            map.retain(|k, _| {
6479                                                                if *k >= watermark {
6480                                                                    true
6481                                                                } else {
6482                                                                    deltas.push((
6483                                                                        ::std::clone::Clone::clone(k),
6484                                                                        ::std::option::Option::None,
6485                                                                    ));
6486                                                                    false
6487                                                                }
6488                                                            });
6489                                                            *opt_curr_watermark = Some(watermark);
6490                                                        }
6491                                                        Some(deltas)
6492                                                    }
6493                                                })
6494                                                -> flatten();
6495                                        },
6496                                        Some(&stmt_id.to_string()),
6497                                    );
6498                                } else {
6499                                    graph_builders.add_dfir_at(
6500                                        &out_location,
6501                                        parse_quote! {
6502                                            #chain_ident = chain();
6503                                            #input_ident
6504                                                -> map(|x| (Some(x), None))
6505                                                -> [0]#chain_ident;
6506                                            #watermark_ident
6507                                                -> map(|watermark| (None, Some(watermark)))
6508                                                -> [1]#chain_ident;
6509
6510                                            #fold_ident = #chain_ident
6511                                                -> #agg_operator::<#lifetime>(|| (::std::collections::HashMap::new(), None), {
6512                                                    let __reduce_keyed_fn = #f_tokens;
6513                                                    move |(map, opt_curr_watermark), (opt_payload, opt_watermark)| {
6514                                                        if let Some((k, v)) = opt_payload {
6515                                                            if let Some(curr_watermark) = *opt_curr_watermark {
6516                                                                if k < curr_watermark {
6517                                                                    return;
6518                                                                }
6519                                                            }
6520                                                            match map.entry(k) {
6521                                                                ::std::collections::hash_map::Entry::Vacant(e) => {
6522                                                                    e.insert(v);
6523                                                                }
6524                                                                ::std::collections::hash_map::Entry::Occupied(mut e) => {
6525                                                                    __reduce_keyed_fn(e.get_mut(), v);
6526                                                                }
6527                                                            }
6528                                                        } else {
6529                                                            let watermark = opt_watermark.unwrap();
6530                                                            if let Some(curr_watermark) = *opt_curr_watermark {
6531                                                                if watermark <= curr_watermark {
6532                                                                    return;
6533                                                                }
6534                                                            }
6535                                                            map.retain(|k, _| *k >= watermark);
6536                                                            *opt_curr_watermark = Some(watermark);
6537                                                        }
6538                                                    }
6539                                                })
6540                                                -> flat_map(|(map, _curr_watermark)| map);
6541                                        },
6542                                        Some(&stmt_id.to_string()),
6543                                    );
6544                                }
6545                            }
6546                            BuildersOrCallback::Callback(_, node_callback) => {
6547                                node_callback(node, next_stmt_id);
6548                            }
6549                        }
6550
6551                        ident_stack.push(fold_ident);
6552                    }
6553
6554                    HydroNode::Network {
6555                        networking_info,
6556                        serialize,
6557                        deserialize,
6558                        instantiate_fn,
6559                        input,
6560                        ..
6561                    } => {
6562                        let input_ident = ident_stack.pop().unwrap();
6563
6564                        let stmt_id = next_stmt_id.get_and_increment();
6565                        let receiver_stream_ident =
6566                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
6567
6568                        // For embedded (external) serialization, this synthesizes only the
6569                        // member-id tag conversions (if any) and passes the raw payload through.
6570                        let serialize_pipeline = serialize.pipeline();
6571                        let deserialize_pipeline = deserialize.pipeline();
6572
6573                        match builders_or_callback {
6574                            BuildersOrCallback::Builders(graph_builders) => {
6575                                let (sink_expr, source_expr) = match instantiate_fn {
6576                                    DebugInstantiate::Building => (
6577                                        syn::parse_quote!(DUMMY_SINK),
6578                                        syn::parse_quote!(DUMMY_SOURCE),
6579                                    ),
6580
6581                                    DebugInstantiate::Finalized(finalized) => {
6582                                        (finalized.sink.clone(), finalized.source.clone())
6583                                    }
6584                                };
6585
6586                                graph_builders.create_network(
6587                                    &input.metadata().location_id,
6588                                    &out_location,
6589                                    input_ident,
6590                                    &receiver_stream_ident,
6591                                    serialize_pipeline.as_ref(),
6592                                    sink_expr,
6593                                    source_expr,
6594                                    deserialize_pipeline.as_ref(),
6595                                    serialize.external_element_type(),
6596                                    stmt_id,
6597                                    networking_info,
6598                                );
6599                            }
6600                            BuildersOrCallback::Callback(_, node_callback) => {
6601                                node_callback(node, next_stmt_id);
6602                            }
6603                        }
6604
6605                        ident_stack.push(receiver_stream_ident);
6606                    }
6607
6608                    HydroNode::ExternalInput {
6609                        instantiate_fn,
6610                        deserialize_fn: deserialize_pipeline,
6611                        ..
6612                    } => {
6613                        let stmt_id = next_stmt_id.get_and_increment();
6614                        let receiver_stream_ident =
6615                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
6616
6617                        match builders_or_callback {
6618                            BuildersOrCallback::Builders(graph_builders) => {
6619                                let (_, source_expr) = match instantiate_fn {
6620                                    DebugInstantiate::Building => (
6621                                        syn::parse_quote!(DUMMY_SINK),
6622                                        syn::parse_quote!(DUMMY_SOURCE),
6623                                    ),
6624
6625                                    DebugInstantiate::Finalized(finalized) => {
6626                                        (finalized.sink.clone(), finalized.source.clone())
6627                                    }
6628                                };
6629
6630                                graph_builders.create_external_source(
6631                                    &out_location,
6632                                    source_expr,
6633                                    &receiver_stream_ident,
6634                                    deserialize_pipeline.as_ref(),
6635                                    stmt_id,
6636                                );
6637                            }
6638                            BuildersOrCallback::Callback(_, node_callback) => {
6639                                node_callback(node, next_stmt_id);
6640                            }
6641                        }
6642
6643                        ident_stack.push(receiver_stream_ident);
6644                    }
6645
6646                    HydroNode::Counter {
6647                        tag,
6648                        duration,
6649                        prefix,
6650                        ..
6651                    } => {
6652                        let input_ident = ident_stack.pop().unwrap();
6653
6654                        let stmt_id = next_stmt_id.get_and_increment();
6655                        let counter_ident =
6656                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
6657
6658                        match builders_or_callback {
6659                            BuildersOrCallback::Builders(graph_builders) => {
6660                                let arg = format!("{}({})", prefix, tag);
6661                                graph_builders.add_dfir_at(
6662                                    &out_location,
6663                                    parse_quote! {
6664                                        #counter_ident = #input_ident -> _counter(#arg, #duration);
6665                                    },
6666                                    Some(&stmt_id.to_string()),
6667                                );
6668                            }
6669                            BuildersOrCallback::Callback(_, node_callback) => {
6670                                node_callback(node, next_stmt_id);
6671                            }
6672                        }
6673
6674                        ident_stack.push(counter_ident);
6675                    }
6676
6677                    HydroNode::VersionedNetworkFork {
6678                        channel_id,
6679                        senders,
6680                        metadata,
6681                        ..
6682                    } => {
6683                        // sender idents are pushed in order of the 'senders' member.
6684                        let split_at = ident_stack.len() - senders.len();
6685                        let sender_idents = ident_stack.split_off(split_at);
6686
6687                        let stmt_id = next_stmt_id.get_and_increment();
6688
6689                        // All senders share the channel, so the raw element type (for embedded
6690                        // serialization) is read from the first sender.
6691                        let external_element_type =
6692                            senders.first().and_then(|(_, _, s)| s.external_element_type());
6693
6694                        match builders_or_callback {
6695                            BuildersOrCallback::Builders(graph_builders) => {
6696                                let sender_args: Vec<(LocationId, syn::Ident, Option<DebugExpr>)> =
6697                                    senders
6698                                        .iter()
6699                                        .zip(sender_idents)
6700                                        .map(|((_version, sender, serialize), ident)| {
6701                                            (
6702                                                sender.metadata().location_id.clone(),
6703                                                ident,
6704                                                serialize.pipeline(),
6705                                            )
6706                                        })
6707                                        .collect();
6708                                graph_builders.create_versioned_network_fork(
6709                                    *channel_id,
6710                                    &metadata.location_id,
6711                                    sender_args,
6712                                    external_element_type,
6713                                    stmt_id,
6714                                );
6715                            }
6716                            BuildersOrCallback::Callback(_, node_callback) => {
6717                                node_callback(node, next_stmt_id);
6718                            }
6719                        }
6720                    }
6721
6722                    HydroNode::VersionedNetwork {
6723                        fork,
6724                        deserialize,
6725                        metadata,
6726                        ..
6727                    } => {
6728                        let stmt_id = next_stmt_id.get_and_increment();
6729                        let receiver_stream_ident =
6730                            syn::Ident::new(&format!("stream_{}", stmt_id), Span::call_site());
6731
6732                        // The wire element type is determined by the channel's *source* kind, which
6733                        // all senders share; read it from the shared fork's first sender.
6734                        let (channel_id, source_loc) = {
6735                            let fork_ref = fork.0.borrow();
6736                            let HydroNode::VersionedNetworkFork {
6737                                channel_id,
6738                                senders,
6739                                ..
6740                            } = &*fork_ref
6741                            else {
6742                                unreachable!("VersionedNetwork.fork must be a VersionedNetworkFork");
6743                            };
6744                            let source_loc = senders
6745                                .first()
6746                                .map(|(_v, sender, _s)| sender.metadata().location_id.clone())
6747                                .expect("a VersionedNetworkFork always has at least one sender");
6748                            (*channel_id, source_loc)
6749                        };
6750
6751                        let deserialize_pipeline = deserialize.pipeline();
6752                        let external_element_type = deserialize.external_element_type();
6753
6754                        match builders_or_callback {
6755                            BuildersOrCallback::Builders(graph_builders) => {
6756                                graph_builders.create_versioned_network(
6757                                    channel_id,
6758                                    &source_loc,
6759                                    &metadata.location_id,
6760                                    &receiver_stream_ident,
6761                                    deserialize_pipeline.as_ref(),
6762                                    external_element_type,
6763                                    stmt_id,
6764                                );
6765                            }
6766                            BuildersOrCallback::Callback(_, node_callback) => {
6767                                node_callback(node, next_stmt_id);
6768                            }
6769                        }
6770
6771                        ident_stack.push(receiver_stream_ident);
6772                    }
6773                }
6774            },
6775            seen_tees,
6776            false,
6777        );
6778
6779        let ret = ident_stack
6780            .pop()
6781            .expect("ident_stack should have exactly one element after traversal");
6782        assert!(
6783            ident_stack.is_empty(),
6784            "ident_stack should be empty after popping the final ident, but has {} remaining element(s). \
6785             This indicates a bug in the code gen: some node pushed idents that were never consumed.",
6786            ident_stack.len()
6787        );
6788        ret
6789    }
6790
6791    pub fn visit_debug_expr(&mut self, mut transform: impl FnMut(&mut DebugExpr)) {
6792        match self {
6793            HydroNode::Placeholder => {
6794                panic!()
6795            }
6796            HydroNode::Cast { .. }
6797            | HydroNode::ObserveNonDet { .. }
6798            | HydroNode::UnboundSingleton { .. }
6799            | HydroNode::AssertIsConsistent { .. } => {}
6800            HydroNode::Source { source, .. } => match source {
6801                HydroSource::Stream(expr) | HydroSource::Iter(expr) => transform(expr),
6802                HydroSource::ExternalNetwork()
6803                | HydroSource::Spin()
6804                | HydroSource::ClusterMembers(_, _)
6805                | HydroSource::Embedded(_)
6806                | HydroSource::EmbeddedSingleton(_) => {} // TODO: what goes here?
6807            },
6808            HydroNode::SingletonSource { value, .. } => {
6809                transform(value);
6810            }
6811            HydroNode::CycleSource { .. }
6812            | HydroNode::Tee { .. }
6813            | HydroNode::Reference { .. }
6814            | HydroNode::YieldConcat { .. }
6815            | HydroNode::BeginAtomic { .. }
6816            | HydroNode::EndAtomic { .. }
6817            | HydroNode::Batch { .. }
6818            | HydroNode::Snapshot { .. }
6819            | HydroNode::Chain { .. }
6820            | HydroNode::MergeOrdered { .. }
6821            | HydroNode::ChainFirst { .. }
6822            | HydroNode::CrossProduct { .. }
6823            | HydroNode::CrossSingleton { .. }
6824            | HydroNode::ResolveFutures { .. }
6825            | HydroNode::ResolveFuturesBlocking { .. }
6826            | HydroNode::ResolveFuturesOrdered { .. }
6827            | HydroNode::Join { .. }
6828            | HydroNode::JoinHalf { .. }
6829            | HydroNode::Difference { .. }
6830            | HydroNode::AntiJoin { .. }
6831            | HydroNode::DeferTick { .. }
6832            | HydroNode::Enumerate { .. }
6833            | HydroNode::Unique { .. }
6834            | HydroNode::Sort { .. }
6835            | HydroNode::PartitionSide { .. }
6836            | HydroNode::VersionedNetworkFork { .. }
6837            | HydroNode::VersionedNetwork { .. } => {}
6838            HydroNode::Map { f, .. }
6839            | HydroNode::FlatMap { f, .. }
6840            | HydroNode::FlatMapStreamBlocking { f, .. }
6841            | HydroNode::Filter { f, .. }
6842            | HydroNode::FilterMap { f, .. }
6843            | HydroNode::Inspect { f, .. }
6844            | HydroNode::PartitionShared { f, .. }
6845            | HydroNode::Reduce { f, .. }
6846            | HydroNode::ReduceKeyed { f, .. }
6847            | HydroNode::ReduceKeyedWatermark { f, .. } => {
6848                transform(&mut f.expr);
6849            }
6850            HydroNode::Fold { init, acc, .. }
6851            | HydroNode::Scan { init, acc, .. }
6852            | HydroNode::ScanAsyncBlocking { init, acc, .. }
6853            | HydroNode::FoldKeyed { init, acc, .. } => {
6854                transform(&mut init.expr);
6855                transform(&mut acc.expr);
6856            }
6857            HydroNode::Network {
6858                serialize,
6859                deserialize,
6860                ..
6861            } => {
6862                if let NetworkSend::Custom {
6863                    serialize_fn: Some(serialize_fn),
6864                } = serialize
6865                {
6866                    transform(serialize_fn);
6867                }
6868                if let NetworkRecv::Custom {
6869                    deserialize_fn: Some(deserialize_fn),
6870                } = deserialize
6871                {
6872                    transform(deserialize_fn);
6873                }
6874            }
6875            HydroNode::ExternalInput { deserialize_fn, .. } => {
6876                if let Some(deserialize_fn) = deserialize_fn {
6877                    transform(deserialize_fn);
6878                }
6879            }
6880            HydroNode::Counter { duration, .. } => {
6881                transform(duration);
6882            }
6883        }
6884    }
6885
6886    pub fn op_metadata(&self) -> &HydroIrOpMetadata {
6887        &self.metadata().op
6888    }
6889
6890    pub fn metadata(&self) -> &HydroIrMetadata {
6891        match self {
6892            HydroNode::Placeholder => {
6893                panic!()
6894            }
6895            HydroNode::VersionedNetworkFork { metadata, .. }
6896            | HydroNode::VersionedNetwork { metadata, .. } => metadata,
6897            HydroNode::Cast { metadata, .. }
6898            | HydroNode::ObserveNonDet { metadata, .. }
6899            | HydroNode::AssertIsConsistent { metadata, .. }
6900            | HydroNode::UnboundSingleton { metadata, .. }
6901            | HydroNode::Source { metadata, .. }
6902            | HydroNode::SingletonSource { metadata, .. }
6903            | HydroNode::CycleSource { metadata, .. }
6904            | HydroNode::Tee { metadata, .. }
6905            | HydroNode::Reference { metadata, .. }
6906            | HydroNode::PartitionSide { metadata, .. }
6907            | HydroNode::PartitionShared { metadata, .. }
6908            | HydroNode::YieldConcat { metadata, .. }
6909            | HydroNode::BeginAtomic { metadata, .. }
6910            | HydroNode::EndAtomic { metadata, .. }
6911            | HydroNode::Batch { metadata, .. }
6912            | HydroNode::Snapshot { metadata, .. }
6913            | HydroNode::Chain { metadata, .. }
6914            | HydroNode::MergeOrdered { metadata, .. }
6915            | HydroNode::ChainFirst { metadata, .. }
6916            | HydroNode::CrossProduct { metadata, .. }
6917            | HydroNode::CrossSingleton { metadata, .. }
6918            | HydroNode::Join { metadata, .. }
6919            | HydroNode::JoinHalf { metadata, .. }
6920            | HydroNode::Difference { metadata, .. }
6921            | HydroNode::AntiJoin { metadata, .. }
6922            | HydroNode::ResolveFutures { metadata, .. }
6923            | HydroNode::ResolveFuturesBlocking { metadata, .. }
6924            | HydroNode::ResolveFuturesOrdered { metadata, .. }
6925            | HydroNode::Map { metadata, .. }
6926            | HydroNode::FlatMap { metadata, .. }
6927            | HydroNode::FlatMapStreamBlocking { metadata, .. }
6928            | HydroNode::Filter { metadata, .. }
6929            | HydroNode::FilterMap { metadata, .. }
6930            | HydroNode::DeferTick { metadata, .. }
6931            | HydroNode::Enumerate { metadata, .. }
6932            | HydroNode::Inspect { metadata, .. }
6933            | HydroNode::Unique { metadata, .. }
6934            | HydroNode::Sort { metadata, .. }
6935            | HydroNode::Scan { metadata, .. }
6936            | HydroNode::ScanAsyncBlocking { metadata, .. }
6937            | HydroNode::Fold { metadata, .. }
6938            | HydroNode::FoldKeyed { metadata, .. }
6939            | HydroNode::Reduce { metadata, .. }
6940            | HydroNode::ReduceKeyed { metadata, .. }
6941            | HydroNode::ReduceKeyedWatermark { metadata, .. }
6942            | HydroNode::ExternalInput { metadata, .. }
6943            | HydroNode::Network { metadata, .. }
6944            | HydroNode::Counter { metadata, .. } => metadata,
6945        }
6946    }
6947
6948    pub fn op_metadata_mut(&mut self) -> &mut HydroIrOpMetadata {
6949        &mut self.metadata_mut().op
6950    }
6951
6952    pub fn metadata_mut(&mut self) -> &mut HydroIrMetadata {
6953        match self {
6954            HydroNode::Placeholder => {
6955                panic!()
6956            }
6957            HydroNode::VersionedNetworkFork { metadata, .. }
6958            | HydroNode::VersionedNetwork { metadata, .. } => metadata,
6959            HydroNode::Cast { metadata, .. }
6960            | HydroNode::ObserveNonDet { metadata, .. }
6961            | HydroNode::AssertIsConsistent { metadata, .. }
6962            | HydroNode::UnboundSingleton { metadata, .. }
6963            | HydroNode::Source { metadata, .. }
6964            | HydroNode::SingletonSource { metadata, .. }
6965            | HydroNode::CycleSource { metadata, .. }
6966            | HydroNode::Tee { metadata, .. }
6967            | HydroNode::Reference { metadata, .. }
6968            | HydroNode::PartitionSide { metadata, .. }
6969            | HydroNode::PartitionShared { metadata, .. }
6970            | HydroNode::YieldConcat { metadata, .. }
6971            | HydroNode::BeginAtomic { metadata, .. }
6972            | HydroNode::EndAtomic { metadata, .. }
6973            | HydroNode::Batch { metadata, .. }
6974            | HydroNode::Snapshot { metadata, .. }
6975            | HydroNode::Chain { metadata, .. }
6976            | HydroNode::MergeOrdered { metadata, .. }
6977            | HydroNode::ChainFirst { metadata, .. }
6978            | HydroNode::CrossProduct { metadata, .. }
6979            | HydroNode::CrossSingleton { metadata, .. }
6980            | HydroNode::Join { metadata, .. }
6981            | HydroNode::JoinHalf { metadata, .. }
6982            | HydroNode::Difference { metadata, .. }
6983            | HydroNode::AntiJoin { metadata, .. }
6984            | HydroNode::ResolveFutures { metadata, .. }
6985            | HydroNode::ResolveFuturesBlocking { metadata, .. }
6986            | HydroNode::ResolveFuturesOrdered { metadata, .. }
6987            | HydroNode::Map { metadata, .. }
6988            | HydroNode::FlatMap { metadata, .. }
6989            | HydroNode::FlatMapStreamBlocking { metadata, .. }
6990            | HydroNode::Filter { metadata, .. }
6991            | HydroNode::FilterMap { metadata, .. }
6992            | HydroNode::DeferTick { metadata, .. }
6993            | HydroNode::Enumerate { metadata, .. }
6994            | HydroNode::Inspect { metadata, .. }
6995            | HydroNode::Unique { metadata, .. }
6996            | HydroNode::Sort { metadata, .. }
6997            | HydroNode::Scan { metadata, .. }
6998            | HydroNode::ScanAsyncBlocking { metadata, .. }
6999            | HydroNode::Fold { metadata, .. }
7000            | HydroNode::FoldKeyed { metadata, .. }
7001            | HydroNode::Reduce { metadata, .. }
7002            | HydroNode::ReduceKeyed { metadata, .. }
7003            | HydroNode::ReduceKeyedWatermark { metadata, .. }
7004            | HydroNode::ExternalInput { metadata, .. }
7005            | HydroNode::Network { metadata, .. }
7006            | HydroNode::Counter { metadata, .. } => metadata,
7007        }
7008    }
7009
7010    pub fn input(&self) -> Vec<&HydroNode> {
7011        match self {
7012            HydroNode::Placeholder => {
7013                panic!()
7014            }
7015            HydroNode::Source { .. }
7016            | HydroNode::SingletonSource { .. }
7017            | HydroNode::ExternalInput { .. }
7018            | HydroNode::CycleSource { .. }
7019            | HydroNode::Tee { .. }
7020            | HydroNode::Reference { .. }
7021            | HydroNode::PartitionSide { .. }
7022            | HydroNode::VersionedNetwork { .. } => {
7023                // Tee/PartitionSide/VersionedNetwork find their input in separate special ways
7024                vec![]
7025            }
7026            HydroNode::Cast { inner, .. }
7027            | HydroNode::ObserveNonDet { inner, .. }
7028            | HydroNode::YieldConcat { inner, .. }
7029            | HydroNode::BeginAtomic { inner, .. }
7030            | HydroNode::EndAtomic { inner, .. }
7031            | HydroNode::Batch { inner, .. }
7032            | HydroNode::Snapshot { inner, .. }
7033            | HydroNode::UnboundSingleton { inner, .. }
7034            | HydroNode::AssertIsConsistent { inner, .. } => {
7035                vec![inner]
7036            }
7037            HydroNode::Chain { first, second, .. }
7038            | HydroNode::MergeOrdered { first, second, .. }
7039            | HydroNode::ChainFirst { first, second, .. } => {
7040                vec![first, second]
7041            }
7042            HydroNode::CrossProduct { left, right, .. }
7043            | HydroNode::CrossSingleton { left, right, .. }
7044            | HydroNode::Join { left, right, .. }
7045            | HydroNode::JoinHalf { left, right, .. } => {
7046                vec![left, right]
7047            }
7048            HydroNode::Difference { pos, neg, .. } | HydroNode::AntiJoin { pos, neg, .. } => {
7049                vec![pos, neg]
7050            }
7051            HydroNode::Counter { input, .. }
7052            | HydroNode::DeferTick { input, .. }
7053            | HydroNode::Enumerate { input, .. }
7054            | HydroNode::Filter { input, .. }
7055            | HydroNode::FilterMap { input, .. }
7056            | HydroNode::FlatMap { input, .. }
7057            | HydroNode::FlatMapStreamBlocking { input, .. }
7058            | HydroNode::Fold { input, .. }
7059            | HydroNode::FoldKeyed { input, .. }
7060            | HydroNode::Inspect { input, .. }
7061            | HydroNode::Map { input, .. }
7062            | HydroNode::Network { input, .. }
7063            | HydroNode::PartitionShared { input, .. }
7064            | HydroNode::Reduce { input, .. }
7065            | HydroNode::ReduceKeyed { input, .. }
7066            | HydroNode::ResolveFutures { input, .. }
7067            | HydroNode::ResolveFuturesBlocking { input, .. }
7068            | HydroNode::ResolveFuturesOrdered { input, .. }
7069            | HydroNode::Scan { input, .. }
7070            | HydroNode::ScanAsyncBlocking { input, .. }
7071            | HydroNode::Sort { input, .. }
7072            | HydroNode::Unique { input, .. } => {
7073                vec![input]
7074            }
7075            HydroNode::ReduceKeyedWatermark {
7076                input, watermark, ..
7077            } => {
7078                vec![input, watermark]
7079            }
7080            HydroNode::VersionedNetworkFork { senders, .. } => senders
7081                .iter()
7082                .map(|(_version, sender, _serialize)| sender.as_ref())
7083                .collect(),
7084        }
7085    }
7086
7087    pub fn input_metadata(&self) -> Vec<&HydroIrMetadata> {
7088        self.input()
7089            .iter()
7090            .map(|input_node| input_node.metadata())
7091            .collect()
7092    }
7093
7094    /// Returns `true` if this node is a Tee or Partition whose inner Rc
7095    /// has other live references, meaning the upstream is already driven
7096    /// by another consumer and does not need a Null sink.
7097    pub fn is_shared_with_others(&self) -> bool {
7098        match self {
7099            HydroNode::Tee { inner, .. } | HydroNode::PartitionSide { inner, .. } => {
7100                Rc::strong_count(&inner.0) > 1
7101            }
7102            // A zero-output reference node is valid in DFIR (it drains itself at
7103            // end of tick), so it doesn't need to be driven by another consumer.
7104            HydroNode::Reference { .. } => false,
7105            _ => false,
7106        }
7107    }
7108
7109    pub fn print_root(&self) -> String {
7110        match self {
7111            HydroNode::Placeholder => {
7112                panic!()
7113            }
7114            HydroNode::Cast { .. } => "Cast()".to_owned(),
7115            HydroNode::UnboundSingleton { .. } => "UnboundSingleton()".to_owned(),
7116            HydroNode::ObserveNonDet { .. } => "ObserveNonDet()".to_owned(),
7117            HydroNode::AssertIsConsistent { .. } => "AssertIsConsistent()".to_owned(),
7118            HydroNode::Source { source, .. } => format!("Source({:?})", source),
7119            HydroNode::SingletonSource {
7120                value,
7121                first_tick_only,
7122                ..
7123            } => format!(
7124                "SingletonSource({:?}, first_tick_only={})",
7125                value, first_tick_only
7126            ),
7127            HydroNode::CycleSource { cycle_id, .. } => format!("CycleSource({})", cycle_id),
7128            HydroNode::Tee { inner, .. } => {
7129                format!("Tee({})", inner.0.borrow().print_root())
7130            }
7131            HydroNode::Reference { inner, kind, .. } => {
7132                format!("Reference({:?}, {})", kind, inner.0.borrow().print_root())
7133            }
7134            HydroNode::PartitionSide { inner, is_true, .. } => {
7135                format!(
7136                    "PartitionSide(is_true={}, {})",
7137                    is_true,
7138                    inner.0.borrow().print_root(),
7139                )
7140            }
7141            HydroNode::PartitionShared { f, .. } => format!("PartitionShared({:?})", f),
7142            HydroNode::YieldConcat { .. } => "YieldConcat()".to_owned(),
7143            HydroNode::BeginAtomic { .. } => "BeginAtomic()".to_owned(),
7144            HydroNode::EndAtomic { .. } => "EndAtomic()".to_owned(),
7145            HydroNode::Batch { .. } => "Batch()".to_owned(),
7146            HydroNode::Snapshot { .. } => "Snapshot()".to_owned(),
7147            HydroNode::Chain { first, second, .. } => {
7148                format!("Chain({}, {})", first.print_root(), second.print_root())
7149            }
7150            HydroNode::MergeOrdered { first, second, .. } => {
7151                format!(
7152                    "MergeOrdered({}, {})",
7153                    first.print_root(),
7154                    second.print_root()
7155                )
7156            }
7157            HydroNode::ChainFirst { first, second, .. } => {
7158                format!(
7159                    "ChainFirst({}, {})",
7160                    first.print_root(),
7161                    second.print_root()
7162                )
7163            }
7164            HydroNode::CrossProduct { left, right, .. } => {
7165                format!(
7166                    "CrossProduct({}, {})",
7167                    left.print_root(),
7168                    right.print_root()
7169                )
7170            }
7171            HydroNode::CrossSingleton { left, right, .. } => {
7172                format!(
7173                    "CrossSingleton({}, {})",
7174                    left.print_root(),
7175                    right.print_root()
7176                )
7177            }
7178            HydroNode::Join { left, right, .. } => {
7179                format!("Join({}, {})", left.print_root(), right.print_root())
7180            }
7181            HydroNode::JoinHalf { left, right, .. } => {
7182                format!("JoinHalf({}, {})", left.print_root(), right.print_root())
7183            }
7184            HydroNode::Difference { pos, neg, .. } => {
7185                format!("Difference({}, {})", pos.print_root(), neg.print_root())
7186            }
7187            HydroNode::AntiJoin { pos, neg, .. } => {
7188                format!("AntiJoin({}, {})", pos.print_root(), neg.print_root())
7189            }
7190            HydroNode::ResolveFutures { .. } => "ResolveFutures()".to_owned(),
7191            HydroNode::ResolveFuturesBlocking { .. } => "ResolveFuturesBlocking()".to_owned(),
7192            HydroNode::ResolveFuturesOrdered { .. } => "ResolveFuturesOrdered()".to_owned(),
7193            HydroNode::Map { f, .. } => format!("Map({:?})", f),
7194            HydroNode::FlatMap { f, .. } => format!("FlatMap({:?})", f),
7195            HydroNode::FlatMapStreamBlocking { f, .. } => format!("FlatMapStreamBlocking({:?})", f),
7196            HydroNode::Filter { f, .. } => format!("Filter({:?})", f),
7197            HydroNode::FilterMap { f, .. } => format!("FilterMap({:?})", f),
7198            HydroNode::DeferTick { .. } => "DeferTick()".to_owned(),
7199            HydroNode::Enumerate { .. } => "Enumerate()".to_owned(),
7200            HydroNode::Inspect { f, .. } => format!("Inspect({:?})", f),
7201            HydroNode::Unique { .. } => "Unique()".to_owned(),
7202            HydroNode::Sort { .. } => "Sort()".to_owned(),
7203            HydroNode::Fold { init, acc, .. } => format!("Fold({:?}, {:?})", init, acc),
7204            HydroNode::Scan { init, acc, .. } => format!("Scan({:?}, {:?})", init, acc),
7205            HydroNode::ScanAsyncBlocking { init, acc, .. } => {
7206                format!("ScanAsyncBlocking({:?}, {:?})", init, acc)
7207            }
7208            HydroNode::FoldKeyed { init, acc, .. } => format!("FoldKeyed({:?}, {:?})", init, acc),
7209            HydroNode::Reduce { f, .. } => format!("Reduce({:?})", f),
7210            HydroNode::ReduceKeyed { f, .. } => format!("ReduceKeyed({:?})", f),
7211            HydroNode::ReduceKeyedWatermark { f, .. } => format!("ReduceKeyedWatermark({:?})", f),
7212            HydroNode::Network { .. } => "Network()".to_owned(),
7213            HydroNode::ExternalInput { .. } => "ExternalInput()".to_owned(),
7214            HydroNode::Counter { tag, duration, .. } => {
7215                format!("Counter({:?}, {:?})", tag, duration)
7216            }
7217            HydroNode::VersionedNetworkFork {
7218                channel_name,
7219                senders,
7220                ..
7221            } => {
7222                let versions: Vec<u32> = senders.iter().map(|(v, _, _)| *v).collect();
7223                format!(
7224                    "VersionedNetworkFork({}, senders={:?})",
7225                    channel_name, versions
7226                )
7227            }
7228            HydroNode::VersionedNetwork { version, .. } => {
7229                format!("VersionedNetwork(v{})", version)
7230            }
7231        }
7232    }
7233}
7234
7235#[cfg(feature = "build")]
7236#[expect(clippy::too_many_arguments, reason = "networking codegen")]
7237fn instantiate_network<'a, D>(
7238    env: &mut D::InstantiateEnv,
7239    from_location: &LocationId,
7240    to_location: &LocationId,
7241    processes: &SparseSecondaryMap<LocationKey, D::Process>,
7242    clusters: &SparseSecondaryMap<LocationKey, D::Cluster>,
7243    name: Option<&str>,
7244    networking_info: &crate::networking::NetworkingInfo,
7245    external_types: Option<(&syn::Type, &syn::Type)>,
7246) -> (syn::Expr, syn::Expr, Box<dyn FnOnce()>)
7247where
7248    D: Deploy<'a>,
7249{
7250    if external_types.is_some() {
7251        assert!(
7252            D::SUPPORTS_EXTERNAL_SERIALIZATION,
7253            "`EMBEDDED` channels are implemented by the host of an embedded program and are \
7254             only supported by the embedded deployment backend (and the simulator). Use `TCP` \
7255             or `UDP` (with `.bincode()`) for this deployment target instead."
7256        );
7257    }
7258
7259    let ((sink, source), connect_fn) = match (from_location, to_location) {
7260        (&LocationId::Process(from), &LocationId::Process(to)) => {
7261            let from_node = processes
7262                .get(from)
7263                .unwrap_or_else(|| {
7264                    panic!("A process used in the graph was not instantiated: {}", from)
7265                })
7266                .clone();
7267            let to_node = processes
7268                .get(to)
7269                .unwrap_or_else(|| {
7270                    panic!("A process used in the graph was not instantiated: {}", to)
7271                })
7272                .clone();
7273
7274            let sink_port = from_node.next_port();
7275            let source_port = to_node.next_port();
7276
7277            (
7278                D::o2o_sink_source(
7279                    env,
7280                    &from_node,
7281                    &sink_port,
7282                    &to_node,
7283                    &source_port,
7284                    name,
7285                    networking_info,
7286                    external_types,
7287                ),
7288                D::o2o_connect(&from_node, &sink_port, &to_node, &source_port),
7289            )
7290        }
7291        (&LocationId::Process(from), &LocationId::Cluster(to)) => {
7292            let from_node = processes
7293                .get(from)
7294                .unwrap_or_else(|| {
7295                    panic!("A process used in the graph was not instantiated: {}", from)
7296                })
7297                .clone();
7298            let to_node = clusters
7299                .get(to)
7300                .unwrap_or_else(|| {
7301                    panic!("A cluster used in the graph was not instantiated: {}", to)
7302                })
7303                .clone();
7304
7305            let sink_port = from_node.next_port();
7306            let source_port = to_node.next_port();
7307
7308            (
7309                D::o2m_sink_source(
7310                    env,
7311                    &from_node,
7312                    &sink_port,
7313                    &to_node,
7314                    &source_port,
7315                    name,
7316                    networking_info,
7317                    external_types,
7318                ),
7319                D::o2m_connect(&from_node, &sink_port, &to_node, &source_port),
7320            )
7321        }
7322        (&LocationId::Cluster(from), &LocationId::Process(to)) => {
7323            let from_node = clusters
7324                .get(from)
7325                .unwrap_or_else(|| {
7326                    panic!("A cluster used in the graph was not instantiated: {}", from)
7327                })
7328                .clone();
7329            let to_node = processes
7330                .get(to)
7331                .unwrap_or_else(|| {
7332                    panic!("A process used in the graph was not instantiated: {}", to)
7333                })
7334                .clone();
7335
7336            let sink_port = from_node.next_port();
7337            let source_port = to_node.next_port();
7338
7339            (
7340                D::m2o_sink_source(
7341                    env,
7342                    &from_node,
7343                    &sink_port,
7344                    &to_node,
7345                    &source_port,
7346                    name,
7347                    networking_info,
7348                    external_types,
7349                ),
7350                D::m2o_connect(&from_node, &sink_port, &to_node, &source_port),
7351            )
7352        }
7353        (&LocationId::Cluster(from), &LocationId::Cluster(to)) => {
7354            let from_node = clusters
7355                .get(from)
7356                .unwrap_or_else(|| {
7357                    panic!("A cluster used in the graph was not instantiated: {}", from)
7358                })
7359                .clone();
7360            let to_node = clusters
7361                .get(to)
7362                .unwrap_or_else(|| {
7363                    panic!("A cluster used in the graph was not instantiated: {}", to)
7364                })
7365                .clone();
7366
7367            let sink_port = from_node.next_port();
7368            let source_port = to_node.next_port();
7369
7370            (
7371                D::m2m_sink_source(
7372                    env,
7373                    &from_node,
7374                    &sink_port,
7375                    &to_node,
7376                    &source_port,
7377                    name,
7378                    networking_info,
7379                    external_types,
7380                ),
7381                D::m2m_connect(&from_node, &sink_port, &to_node, &source_port),
7382            )
7383        }
7384        (LocationId::Tick { .. }, _) => panic!(),
7385        (_, LocationId::Tick { .. }) => panic!(),
7386        (LocationId::Atomic(_), _) => panic!(),
7387        (_, LocationId::Atomic(_)) => panic!(),
7388    };
7389    (sink, source, connect_fn)
7390}
7391
7392#[cfg(test)]
7393mod serde_test;
7394
7395#[cfg(test)]
7396mod test {
7397    use std::mem::size_of;
7398
7399    use stageleft::{QuotedWithContext, q};
7400
7401    use super::*;
7402
7403    #[test]
7404    #[cfg_attr(
7405        not(feature = "build"),
7406        ignore = "expects inclusion of feature-gated fields"
7407    )]
7408    fn hydro_node_size() {
7409        assert_eq!(size_of::<HydroNode>(), 280);
7410    }
7411
7412    #[test]
7413    #[cfg_attr(
7414        not(feature = "build"),
7415        ignore = "expects inclusion of feature-gated fields"
7416    )]
7417    fn hydro_root_size() {
7418        assert_eq!(size_of::<HydroRoot>(), 152);
7419    }
7420
7421    #[test]
7422    fn test_simplify_q_macro_basic() {
7423        // Test basic non-q! expression
7424        let simple_expr: syn::Expr = syn::parse_str("x + y").unwrap();
7425        let result = simplify_q_macro(simple_expr.clone());
7426        assert_eq!(result, simple_expr);
7427    }
7428
7429    #[test]
7430    fn test_simplify_q_macro_actual_stageleft_call() {
7431        // Test a simplified version of what a real stageleft call might look like
7432        let stageleft_call = q!(|x: usize| x + 1).splice_fn1_ctx(&());
7433        let result = simplify_q_macro(stageleft_call);
7434        // This should be processed by our visitor and simplified to q!(...)
7435        // since we detect the stageleft::runtime_support::fn_* pattern
7436        hydro_build_utils::assert_snapshot!(result.to_token_stream().to_string());
7437    }
7438
7439    #[test]
7440    fn test_closure_no_pipe_at_start() {
7441        // Test a closure that does not start with a pipe
7442        let stageleft_call = q!({
7443            let foo = 123;
7444            move |b: usize| b + foo
7445        })
7446        .splice_fn1_ctx(&());
7447        let result = simplify_q_macro(stageleft_call);
7448        hydro_build_utils::assert_snapshot!(result.to_token_stream().to_string());
7449    }
7450}