Skip to main content

hydro_lang/compile/
embedded.rs

1//! "Embedded" deployment backend for Hydro.
2//!
3//! Instead of compiling each location into a standalone binary, this backend generates
4//! a Rust source file containing one function per location. Each function returns a
5//! `dfir_rs::scheduled::graph::Dfir` that can be manually driven by the caller.
6//!
7//! This is useful when you want full control over where and how the projected DFIR
8//! code runs (e.g. embedding it into an existing application).
9//!
10//! # Networking
11//!
12//! Process and cluster networking (o2o, o2m, m2o, m2m) is supported. When a location has network
13//! sends or receives, the generated function takes additional `network_out` and
14//! `network_in` parameters whose types are generated structs with one field per
15//! network port (named after the channel). Network channels must be named via
16//! `.name()` on the networking config.
17//!
18//! - Sinks (`EmbeddedNetworkOut`): one `FnMut(..)` field per outgoing channel.
19//! - Sources (`EmbeddedNetworkIn`): one `Stream` field per incoming channel.
20//!
21//! The exact field types depend on the channel's serialization:
22//! - With [`bincode`](crate::networking::Bincode) serialization, Hydro serializes to bytes, so
23//!   sinks are `FnMut(Bytes)` and sources are `Stream<Item = Result<BytesMut, io::Error>>`.
24//! - With [`embedded`](crate::networking::Embedded) serialization, the raw element type `T` flows
25//!   across the channel, so sinks are `FnMut(T)` and sources are `Stream<Item = T>` (with no
26//!   transport `Result` — the caller decides how to handle faults).
27//!
28//! Channels keyed by a cluster member (demux / cluster send) additionally carry a
29//! `TaglessMemberId` alongside the payload in these types.
30//!
31//! The caller is responsible for wiring these together (e.g. via in-memory channels,
32//! sockets, etc.). External ports are not supported.
33
34use std::future::Future;
35use std::io::Error;
36use std::pin::Pin;
37
38use bytes::{Bytes, BytesMut};
39use dfir_lang::diagnostic::Diagnostics;
40use dfir_lang::graph::{AsCodeOptions, DfirGraph};
41use futures::{Sink, Stream};
42use proc_macro2::Span;
43use quote::quote;
44use serde::Serialize;
45use serde::de::DeserializeOwned;
46use slotmap::SparseSecondaryMap;
47use stageleft::{QuotedWithContext, q};
48
49use super::deploy_provider::{ClusterSpec, Deploy, ExternalSpec, Node, ProcessSpec, RegisterPort};
50use crate::compile::builder::ExternalPortId;
51use crate::location::dynamic::LocationId;
52use crate::location::member_id::TaglessMemberId;
53use crate::location::{LocationKey, MembershipEvent, NetworkHint};
54
55/// Marker type for the embedded deployment backend.
56///
57/// All networking methods panic — this backend only supports pure local computation.
58pub enum EmbeddedDeploy {}
59
60/// A trivial node type for embedded deployment. Stores a user-provided function name.
61#[derive(Clone)]
62pub struct EmbeddedNode {
63    /// The function name to use in the generated code for this location.
64    pub fn_name: String,
65    /// The location key for this node, used to register network ports.
66    pub location_key: LocationKey,
67}
68
69impl Node for EmbeddedNode {
70    type Port = ();
71    type Meta = ();
72    type InstantiateEnv = EmbeddedInstantiateEnv;
73
74    fn next_port(&self) -> Self::Port {}
75
76    fn update_meta(&self, _meta: &Self::Meta) {}
77
78    fn instantiate(
79        &self,
80        _env: &mut Self::InstantiateEnv,
81        _meta: &mut Self::Meta,
82        _graph: DfirGraph,
83        _extra_stmts: &[syn::Stmt],
84        _sidecars: &[syn::Expr],
85        _as_code_options: &AsCodeOptions,
86    ) {
87        // No-op: embedded mode doesn't instantiate nodes at deploy time.
88    }
89}
90
91impl<'a> RegisterPort<'a, EmbeddedDeploy> for EmbeddedNode {
92    fn register(&self, _external_port_id: ExternalPortId, _port: Self::Port) {
93        panic!("EmbeddedDeploy does not support external ports");
94    }
95
96    #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
97    fn as_bytes_bidi(
98        &self,
99        _external_port_id: ExternalPortId,
100    ) -> impl Future<
101        Output = super::deploy_provider::DynSourceSink<Result<BytesMut, Error>, Bytes, Error>,
102    > + 'a {
103        async { panic!("EmbeddedDeploy does not support external ports") }
104    }
105
106    #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
107    fn as_bincode_bidi<InT, OutT>(
108        &self,
109        _external_port_id: ExternalPortId,
110    ) -> impl Future<Output = super::deploy_provider::DynSourceSink<OutT, InT, Error>> + 'a
111    where
112        InT: Serialize + 'static,
113        OutT: DeserializeOwned + 'static,
114    {
115        async { panic!("EmbeddedDeploy does not support external ports") }
116    }
117
118    #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
119    fn as_bincode_sink<T>(
120        &self,
121        _external_port_id: ExternalPortId,
122    ) -> impl Future<Output = Pin<Box<dyn Sink<T, Error = Error>>>> + 'a
123    where
124        T: Serialize + 'static,
125    {
126        async { panic!("EmbeddedDeploy does not support external ports") }
127    }
128
129    #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
130    fn as_bincode_source<T>(
131        &self,
132        _external_port_id: ExternalPortId,
133    ) -> impl Future<Output = Pin<Box<dyn Stream<Item = T>>>> + 'a
134    where
135        T: DeserializeOwned + 'static,
136    {
137        async { panic!("EmbeddedDeploy does not support external ports") }
138    }
139}
140
141impl<S: Into<String>> ProcessSpec<'_, EmbeddedDeploy> for S {
142    fn build(self, location_key: LocationKey, _name_hint: &str) -> EmbeddedNode {
143        EmbeddedNode {
144            fn_name: self.into(),
145            location_key,
146        }
147    }
148}
149
150impl<S: Into<String>> ClusterSpec<'_, EmbeddedDeploy> for S {
151    fn build(self, location_key: LocationKey, _name_hint: &str) -> EmbeddedNode {
152        EmbeddedNode {
153            fn_name: self.into(),
154            location_key,
155        }
156    }
157}
158
159impl<S: Into<String>> ExternalSpec<'_, EmbeddedDeploy> for S {
160    fn build(self, location_key: LocationKey, _name_hint: &str) -> EmbeddedNode {
161        EmbeddedNode {
162            fn_name: self.into(),
163            location_key,
164        }
165    }
166}
167
168/// Collected embedded input/output registrations, keyed by location.
169///
170/// During `compile_network`, each `HydroSource::Embedded` and `HydroRoot::EmbeddedOutput`
171/// IR node registers its ident, element type, and location key here.
172/// `generate_embedded` then uses this to add the appropriate parameters
173/// to each generated function.
174#[derive(Default)]
175pub struct EmbeddedInstantiateEnv {
176    /// (ident name, element type) pairs per location key, for inputs.
177    pub inputs: SparseSecondaryMap<LocationKey, Vec<(syn::Ident, syn::Type)>>,
178    /// (ident name, element type) pairs per location key, for singleton inputs.
179    pub singleton_inputs: SparseSecondaryMap<LocationKey, Vec<(syn::Ident, syn::Type)>>,
180    /// (ident name, element type) pairs per location key, for outputs.
181    pub outputs: SparseSecondaryMap<LocationKey, Vec<(syn::Ident, syn::Type)>>,
182    /// Network output port names per location key (sender side of channels).
183    /// Each entry is `(port_name, is_tagged, external_type)`. `is_tagged` means the sink is keyed
184    /// by a `TaglessMemberId`. `external_type` distinguishes the two serialization modes:
185    /// - `None` (bincode): the sink is `FnMut(Bytes)` (or `FnMut((TaglessMemberId, Bytes))` when
186    ///   tagged) — Hydro serializes to `Bytes` before the sink.
187    /// - `Some(payload)` (embedded, see [`crate::networking::Embedded`]): the sink receives the raw
188    ///   payload — `FnMut(payload)` (or `FnMut((TaglessMemberId, payload))` when tagged).
189    pub network_outputs: SparseSecondaryMap<LocationKey, Vec<(String, bool, Option<syn::Type>)>>,
190    /// Network input port names per location key (receiver side of channels).
191    /// Each entry is `(port_name, is_tagged, external_type)`. `is_tagged` means the source is keyed
192    /// by a `TaglessMemberId`. `external_type` distinguishes the two serialization modes:
193    /// - `None` (bincode): the source is `Stream<Item = Result<BytesMut, Error>>` (or
194    ///   `Result<(TaglessMemberId, BytesMut), Error>` when tagged) — the transport `Result` is
195    ///   unwrapped and the `BytesMut` deserialized by Hydro.
196    /// - `Some(payload)` (embedded, see [`crate::networking::Embedded`]): the source delivers the
197    ///   raw payload with no transport `Result` — `Stream<Item = payload>` (or
198    ///   `Stream<Item = (TaglessMemberId, payload)>` when tagged); handling faults is left to the
199    ///   external code that produces the stream.
200    pub network_inputs: SparseSecondaryMap<LocationKey, Vec<(String, bool, Option<syn::Type>)>>,
201    /// Cluster membership streams needed per location key.
202    /// Maps location_key -> vec of cluster LocationKeys whose membership is needed.
203    pub membership_streams: SparseSecondaryMap<LocationKey, Vec<LocationKey>>,
204}
205
206impl<'a> Deploy<'a> for EmbeddedDeploy {
207    type Meta = ();
208    type InstantiateEnv = EmbeddedInstantiateEnv;
209
210    type Process = EmbeddedNode;
211    type Cluster = EmbeddedNode;
212    type External = EmbeddedNode;
213
214    const SUPPORTS_EXTERNAL_SERIALIZATION: bool = true;
215
216    fn o2o_sink_source(
217        env: &mut Self::InstantiateEnv,
218        p1: &Self::Process,
219        _p1_port: &(),
220        p2: &Self::Process,
221        _p2_port: &(),
222        name: Option<&str>,
223        _networking_info: &crate::networking::NetworkingInfo,
224        external_types: Option<(&syn::Type, &syn::Type)>,
225    ) -> (syn::Expr, syn::Expr) {
226        let name = name.expect(
227            "EmbeddedDeploy o2o networking requires a channel name. Use `TCP.name(\"my_channel\")` to provide one.",
228        );
229
230        let sink_ident = syn::Ident::new(&format!("__network_out_{name}"), Span::call_site());
231        let source_ident = syn::Ident::new(&format!("__network_in_{name}"), Span::call_site());
232
233        env.network_outputs
234            .entry(p1.location_key)
235            .unwrap()
236            .or_default()
237            .push((
238                name.to_owned(),
239                false,
240                external_types.map(|(i, _)| i.clone()),
241            ));
242        env.network_inputs
243            .entry(p2.location_key)
244            .unwrap()
245            .or_default()
246            .push((
247                name.to_owned(),
248                false,
249                external_types.map(|(_, o)| o.clone()),
250            ));
251
252        (
253            syn::parse_quote!(__root_dfir_rs::sinktools::for_each(#sink_ident)),
254            syn::parse_quote!(#source_ident),
255        )
256    }
257
258    fn o2o_connect(
259        _p1: &Self::Process,
260        _p1_port: &(),
261        _p2: &Self::Process,
262        _p2_port: &(),
263    ) -> Box<dyn FnOnce()> {
264        Box::new(|| {})
265    }
266
267    fn o2m_sink_source(
268        env: &mut Self::InstantiateEnv,
269        p1: &Self::Process,
270        _p1_port: &(),
271        c2: &Self::Cluster,
272        _c2_port: &(),
273        name: Option<&str>,
274        _networking_info: &crate::networking::NetworkingInfo,
275        external_types: Option<(&syn::Type, &syn::Type)>,
276    ) -> (syn::Expr, syn::Expr) {
277        let name = name.expect("EmbeddedDeploy o2m networking requires a channel name.");
278        let sink_ident = syn::Ident::new(&format!("__network_out_{name}"), Span::call_site());
279        let source_ident = syn::Ident::new(&format!("__network_in_{name}"), Span::call_site());
280        env.network_outputs
281            .entry(p1.location_key)
282            .unwrap()
283            .or_default()
284            .push((
285                name.to_owned(),
286                true,
287                external_types.map(|(i, _)| i.clone()),
288            ));
289        env.network_inputs
290            .entry(c2.location_key)
291            .unwrap()
292            .or_default()
293            .push((
294                name.to_owned(),
295                false,
296                external_types.map(|(_, o)| o.clone()),
297            ));
298        (
299            syn::parse_quote!(__root_dfir_rs::sinktools::for_each(#sink_ident)),
300            syn::parse_quote!(#source_ident),
301        )
302    }
303
304    fn o2m_connect(
305        _p1: &Self::Process,
306        _p1_port: &(),
307        _c2: &Self::Cluster,
308        _c2_port: &(),
309    ) -> Box<dyn FnOnce()> {
310        Box::new(|| {})
311    }
312
313    fn m2o_sink_source(
314        env: &mut Self::InstantiateEnv,
315        c1: &Self::Cluster,
316        _c1_port: &(),
317        p2: &Self::Process,
318        _p2_port: &(),
319        name: Option<&str>,
320        _networking_info: &crate::networking::NetworkingInfo,
321        external_types: Option<(&syn::Type, &syn::Type)>,
322    ) -> (syn::Expr, syn::Expr) {
323        let name = name.expect("EmbeddedDeploy m2o networking requires a channel name.");
324        let sink_ident = syn::Ident::new(&format!("__network_out_{name}"), Span::call_site());
325        let source_ident = syn::Ident::new(&format!("__network_in_{name}"), Span::call_site());
326        env.network_outputs
327            .entry(c1.location_key)
328            .unwrap()
329            .or_default()
330            .push((
331                name.to_owned(),
332                false,
333                external_types.map(|(i, _)| i.clone()),
334            ));
335        env.network_inputs
336            .entry(p2.location_key)
337            .unwrap()
338            .or_default()
339            .push((
340                name.to_owned(),
341                true,
342                external_types.map(|(_, o)| o.clone()),
343            ));
344        (
345            syn::parse_quote!(__root_dfir_rs::sinktools::for_each(#sink_ident)),
346            syn::parse_quote!(#source_ident),
347        )
348    }
349
350    fn m2o_connect(
351        _c1: &Self::Cluster,
352        _c1_port: &(),
353        _p2: &Self::Process,
354        _p2_port: &(),
355    ) -> Box<dyn FnOnce()> {
356        Box::new(|| {})
357    }
358
359    fn m2m_sink_source(
360        env: &mut Self::InstantiateEnv,
361        c1: &Self::Cluster,
362        _c1_port: &(),
363        c2: &Self::Cluster,
364        _c2_port: &(),
365        name: Option<&str>,
366        _networking_info: &crate::networking::NetworkingInfo,
367        external_types: Option<(&syn::Type, &syn::Type)>,
368    ) -> (syn::Expr, syn::Expr) {
369        let name = name.expect("EmbeddedDeploy m2m networking requires a channel name.");
370        let sink_ident = syn::Ident::new(&format!("__network_out_{name}"), Span::call_site());
371        let source_ident = syn::Ident::new(&format!("__network_in_{name}"), Span::call_site());
372        env.network_outputs
373            .entry(c1.location_key)
374            .unwrap()
375            .or_default()
376            .push((
377                name.to_owned(),
378                true,
379                external_types.map(|(i, _)| i.clone()),
380            ));
381        env.network_inputs
382            .entry(c2.location_key)
383            .unwrap()
384            .or_default()
385            .push((
386                name.to_owned(),
387                true,
388                external_types.map(|(_, o)| o.clone()),
389            ));
390        (
391            syn::parse_quote!(__root_dfir_rs::sinktools::for_each(#sink_ident)),
392            syn::parse_quote!(#source_ident),
393        )
394    }
395
396    fn m2m_connect(
397        _c1: &Self::Cluster,
398        _c1_port: &(),
399        _c2: &Self::Cluster,
400        _c2_port: &(),
401    ) -> Box<dyn FnOnce()> {
402        Box::new(|| {})
403    }
404
405    fn e2o_many_source(
406        _extra_stmts: &mut Vec<syn::Stmt>,
407        _p2: &Self::Process,
408        _p2_port: &(),
409        _codec_type: &syn::Type,
410        _shared_handle: String,
411    ) -> syn::Expr {
412        panic!("EmbeddedDeploy does not support networking (e2o)")
413    }
414
415    fn e2o_many_sink(_shared_handle: String) -> syn::Expr {
416        panic!("EmbeddedDeploy does not support networking (e2o)")
417    }
418
419    fn e2o_source(
420        _extra_stmts: &mut Vec<syn::Stmt>,
421        _p1: &Self::External,
422        _p1_port: &(),
423        _p2: &Self::Process,
424        _p2_port: &(),
425        _codec_type: &syn::Type,
426        _shared_handle: String,
427    ) -> syn::Expr {
428        panic!("EmbeddedDeploy does not support networking (e2o)")
429    }
430
431    fn e2o_connect(
432        _p1: &Self::External,
433        _p1_port: &(),
434        _p2: &Self::Process,
435        _p2_port: &(),
436        _many: bool,
437        _server_hint: NetworkHint,
438    ) -> Box<dyn FnOnce()> {
439        panic!("EmbeddedDeploy does not support networking (e2o)")
440    }
441
442    fn o2e_sink(
443        _p1: &Self::Process,
444        _p1_port: &(),
445        _p2: &Self::External,
446        _p2_port: &(),
447        _shared_handle: String,
448    ) -> syn::Expr {
449        panic!("EmbeddedDeploy does not support networking (o2e)")
450    }
451
452    #[expect(
453        unreachable_code,
454        reason = "panic before q! which is only for return type"
455    )]
456    fn cluster_ids(
457        _of_cluster: LocationKey,
458    ) -> impl QuotedWithContext<'a, &'a [TaglessMemberId], ()> + Clone + 'a {
459        panic!("EmbeddedDeploy does not support cluster IDs");
460        q!(unreachable!("EmbeddedDeploy does not support cluster IDs"))
461    }
462
463    fn cluster_self_id() -> impl QuotedWithContext<'a, TaglessMemberId, ()> + Clone + 'a {
464        super::embedded_runtime::embedded_cluster_self_id()
465    }
466
467    fn cluster_membership_stream(
468        env: &mut Self::InstantiateEnv,
469        at_location: &LocationId,
470        location_id: &LocationId,
471    ) -> impl QuotedWithContext<'a, Box<dyn Stream<Item = (TaglessMemberId, MembershipEvent)> + Unpin>, ()>
472    {
473        let at_key = match at_location {
474            LocationId::Process(key) | LocationId::Cluster(key) => *key,
475            _ => panic!("cluster_membership_stream must be called from a process or cluster"),
476        };
477        let cluster_key = match location_id {
478            LocationId::Cluster(key) => *key,
479            _ => panic!("cluster_membership_stream target must be a cluster"),
480        };
481        let vec = env.membership_streams.entry(at_key).unwrap().or_default();
482        let idx = if let Some(pos) = vec.iter().position(|k| *k == cluster_key) {
483            pos
484        } else {
485            vec.push(cluster_key);
486            vec.len() - 1
487        };
488
489        super::embedded_runtime::embedded_cluster_membership_stream(idx)
490    }
491
492    fn register_embedded_stream_input(
493        env: &mut Self::InstantiateEnv,
494        location_key: LocationKey,
495        ident: &syn::Ident,
496        element_type: &syn::Type,
497    ) {
498        env.inputs
499            .entry(location_key)
500            .unwrap()
501            .or_default()
502            .push((ident.clone(), element_type.clone()));
503    }
504
505    fn register_embedded_singleton_input(
506        env: &mut Self::InstantiateEnv,
507        location_key: LocationKey,
508        ident: &syn::Ident,
509        element_type: &syn::Type,
510    ) {
511        env.singleton_inputs
512            .entry(location_key)
513            .unwrap()
514            .or_default()
515            .push((ident.clone(), element_type.clone()));
516    }
517
518    fn register_embedded_output(
519        env: &mut Self::InstantiateEnv,
520        location_key: LocationKey,
521        ident: &syn::Ident,
522        element_type: &syn::Type,
523    ) {
524        env.outputs
525            .entry(location_key)
526            .unwrap()
527            .or_default()
528            .push((ident.clone(), element_type.clone()));
529    }
530}
531
532impl super::deploy::DeployFlow<'_, EmbeddedDeploy> {
533    /// Generates a `syn::File` containing one function per location in the flow.
534    ///
535    /// Each generated function has the signature:
536    /// ```ignore
537    /// pub fn <fn_name>() -> dfir_rs::scheduled::graph::Dfir<'static>
538    /// ```
539    /// where `fn_name` is the `String` passed to `with_process` / `with_cluster`.
540    ///
541    /// The returned `Dfir` can be manually executed by the caller.
542    ///
543    /// # Arguments
544    ///
545    /// * `crate_name` — the name of the crate containing the Hydro program (used for stageleft
546    ///   re-exports). Hyphens will be replaced with underscores.
547    ///
548    /// # Usage
549    ///
550    /// Typically called from a `build.rs` in a wrapper crate:
551    /// ```ignore
552    /// // build.rs
553    /// let deploy = flow.with_process(&process, "my_fn".to_string());
554    /// let code = deploy.generate_embedded("my_hydro_crate");
555    /// let out_dir = std::env::var("OUT_DIR").unwrap();
556    /// std::fs::write(format!("{out_dir}/embedded.rs"), prettyplease::unparse(&code)).unwrap();
557    /// ```
558    ///
559    /// Then in `lib.rs`:
560    /// ```ignore
561    /// include!(concat!(env!("OUT_DIR"), "/embedded.rs"));
562    /// ```
563    pub fn generate_embedded(mut self, crate_name: &str) -> syn::File {
564        let mut env = EmbeddedInstantiateEnv::default();
565        let compiled = self.compile_internal(&mut env);
566
567        let root = crate::staging_util::get_this_crate();
568        let orig_crate_name = quote::format_ident!("{}", crate_name.replace('-', "_"));
569
570        let mut items: Vec<syn::Item> = Vec::new();
571
572        // Sort location keys for deterministic output.
573        let mut location_keys: Vec<_> = compiled.all_dfir().keys().collect();
574        location_keys.sort();
575
576        // Build a map from location key to fn_name for lookups.
577        let fn_names: SparseSecondaryMap<LocationKey, &str> = location_keys
578            .iter()
579            .map(|&k| {
580                let name = self
581                    .processes
582                    .get(k)
583                    .map(|n| n.fn_name.as_str())
584                    .or_else(|| self.clusters.get(k).map(|n| n.fn_name.as_str()))
585                    .or_else(|| self.externals.get(k).map(|n| n.fn_name.as_str()))
586                    .expect("location key not found in any node map");
587                (k, name)
588            })
589            .collect();
590
591        for location_key in location_keys {
592            let graph = compiled.all_dfir()[location_key]
593                .as_ref()
594                .unwrap_or_else(|err| {
595                    panic!(
596                        "Failed to partition DFIR graph for location {location_key}: {}",
597                        err.diagnostic
598                    )
599                });
600
601            // Get the user-provided function name from the node.
602            let fn_name = fn_names[location_key];
603            let fn_ident = syn::Ident::new(fn_name, Span::call_site());
604
605            // Get inputs for this location, sorted by name.
606            let mut loc_inputs = env.inputs.get(location_key).cloned().unwrap_or_default();
607            loc_inputs.sort_by(|a, b| a.0.cmp(&b.0));
608
609            // Get outputs for this location, sorted by name.
610            let mut loc_outputs = env.outputs.get(location_key).cloned().unwrap_or_default();
611            loc_outputs.sort_by(|a, b| a.0.cmp(&b.0));
612
613            let mut diagnostics = Diagnostics::new();
614            // `as_code_options` is sparse: it only has entries for locations where a sidecar
615            // edited the options. Locations without sidecars fall back to the defaults.
616            let default_as_code_options = AsCodeOptions::default();
617            let as_code_options = compiled
618                .as_code_options
619                .get(location_key)
620                .unwrap_or(&default_as_code_options);
621            let dfir_tokens = graph
622                .as_code_with_options(
623                    &quote! { __root_dfir_rs },
624                    as_code_options,
625                    quote!(),
626                    &mut diagnostics,
627                )
628                .expect("DFIR inline code generation failed with diagnostics.");
629
630            // --- Build module items (cluster info, output struct, network structs) ---
631            let mut mod_items: Vec<proc_macro2::TokenStream> = Vec::new();
632            let mut extra_fn_generics: Vec<proc_macro2::TokenStream> = Vec::new();
633            let mut cluster_params: Vec<proc_macro2::TokenStream> = Vec::new();
634            let mut output_params: Vec<proc_macro2::TokenStream> = Vec::new();
635            let mut net_out_params: Vec<proc_macro2::TokenStream> = Vec::new();
636            let mut net_in_params: Vec<proc_macro2::TokenStream> = Vec::new();
637            let mut extra_destructure: Vec<proc_macro2::TokenStream> = Vec::new();
638
639            // For cluster locations, add self_id parameter.
640            if self.clusters.contains_key(location_key) {
641                cluster_params.push(quote! {
642                    __cluster_self_id: &'a #root::location::member_id::TaglessMemberId
643                });
644                // Alias to the name the generated DFIR code expects.
645                let self_id_ident = syn::Ident::new(
646                    &format!("__hydro_lang_cluster_self_id_{}", location_key),
647                    Span::call_site(),
648                );
649                extra_destructure.push(quote! {
650                    let #self_id_ident = __cluster_self_id;
651                });
652            }
653
654            // For any location that needs cluster membership streams, add parameters.
655            if let Some(loc_memberships) = env.membership_streams.get(location_key) {
656                let membership_struct_ident =
657                    syn::Ident::new("EmbeddedMembershipStreams", Span::call_site());
658
659                let mem_generic_idents: Vec<syn::Ident> = loc_memberships
660                    .iter()
661                    .enumerate()
662                    .map(|(i, _)| quote::format_ident!("__Mem{}", i))
663                    .collect();
664
665                let mem_field_names: Vec<syn::Ident> = loc_memberships
666                    .iter()
667                    .map(|k| {
668                        let cluster_fn_name = fn_names[*k];
669                        syn::Ident::new(cluster_fn_name, Span::call_site())
670                    })
671                    .collect();
672
673                let struct_fields: Vec<proc_macro2::TokenStream> = mem_field_names
674                    .iter()
675                    .zip(mem_generic_idents.iter())
676                    .map(|(field, generic)| {
677                        quote! { pub #field: #generic }
678                    })
679                    .collect();
680
681                let struct_generics: Vec<proc_macro2::TokenStream> = mem_generic_idents
682                    .iter()
683                    .map(|generic| {
684                        quote! { #generic: __root_dfir_rs::futures::Stream<Item = (#root::location::member_id::TaglessMemberId, #root::location::MembershipEvent)> + Unpin }
685                    })
686                    .collect();
687
688                for generic in &mem_generic_idents {
689                    extra_fn_generics.push(
690                        quote! { #generic: __root_dfir_rs::futures::Stream<Item = (#root::location::member_id::TaglessMemberId, #root::location::MembershipEvent)> + Unpin + 'a },
691                    );
692                }
693
694                cluster_params.push(quote! {
695                    __membership: #fn_ident::#membership_struct_ident<#(#mem_generic_idents),*>
696                });
697
698                for (i, field) in mem_field_names.iter().enumerate() {
699                    let var_ident =
700                        syn::Ident::new(&format!("__membership_{}", i), Span::call_site());
701                    extra_destructure.push(quote! {
702                        let #var_ident = __membership.#field;
703                    });
704                }
705
706                mod_items.push(quote! {
707                    pub struct #membership_struct_ident<#(#struct_generics),*> {
708                        #(#struct_fields),*
709                    }
710                });
711            }
712
713            // Embedded inputs (Stream sources).
714            let input_params: Vec<proc_macro2::TokenStream> = loc_inputs
715                .iter()
716                .map(|(ident, element_type)| {
717                    quote! { #ident: impl __root_dfir_rs::futures::Stream<Item = #element_type> + Unpin + 'a }
718                })
719                .collect();
720
721            // Embedded singleton inputs (plain value parameters).
722            let mut loc_singleton_inputs = env
723                .singleton_inputs
724                .get(location_key)
725                .cloned()
726                .unwrap_or_default();
727            loc_singleton_inputs.sort_by(|a, b| a.0.cmp(&b.0));
728
729            let singleton_input_params: Vec<proc_macro2::TokenStream> = loc_singleton_inputs
730                .iter()
731                .map(|(ident, element_type)| {
732                    quote! { #ident: #element_type }
733                })
734                .collect();
735
736            // Embedded outputs (FnMut callbacks).
737            if !loc_outputs.is_empty() {
738                let output_struct_ident = syn::Ident::new("EmbeddedOutputs", Span::call_site());
739
740                let output_generic_idents: Vec<syn::Ident> = loc_outputs
741                    .iter()
742                    .enumerate()
743                    .map(|(i, _)| quote::format_ident!("__Out{}", i))
744                    .collect();
745
746                let struct_fields: Vec<proc_macro2::TokenStream> = loc_outputs
747                    .iter()
748                    .zip(output_generic_idents.iter())
749                    .map(|((ident, _), generic)| {
750                        quote! { pub #ident: #generic }
751                    })
752                    .collect();
753
754                let struct_generics: Vec<proc_macro2::TokenStream> = loc_outputs
755                    .iter()
756                    .zip(output_generic_idents.iter())
757                    .map(|((_, element_type), generic)| {
758                        quote! { #generic: FnMut(#element_type) }
759                    })
760                    .collect();
761
762                for ((_, element_type), generic) in
763                    loc_outputs.iter().zip(output_generic_idents.iter())
764                {
765                    extra_fn_generics.push(quote! { #generic: FnMut(#element_type) + 'a });
766                }
767
768                output_params.push(quote! {
769                    __outputs: &'a mut #fn_ident::#output_struct_ident<#(#output_generic_idents),*>
770                });
771
772                for (ident, _) in &loc_outputs {
773                    extra_destructure.push(quote! { let mut #ident = &mut __outputs.#ident; });
774                }
775
776                mod_items.push(quote! {
777                    pub struct #output_struct_ident<#(#struct_generics),*> {
778                        #(#struct_fields),*
779                    }
780                });
781            }
782
783            // Network outputs (FnMut sinks).
784            if let Some(mut loc_net_outputs) = env.network_outputs.remove(location_key) {
785                loc_net_outputs.sort_by(|a, b| a.0.cmp(&b.0));
786
787                let net_out_struct_ident = syn::Ident::new("EmbeddedNetworkOut", Span::call_site());
788
789                let net_out_generic_idents: Vec<syn::Ident> = loc_net_outputs
790                    .iter()
791                    .enumerate()
792                    .map(|(i, _)| quote::format_ident!("__NetOut{}", i))
793                    .collect();
794
795                let struct_fields: Vec<proc_macro2::TokenStream> = loc_net_outputs
796                    .iter()
797                    .zip(net_out_generic_idents.iter())
798                    .map(|((name, _, _), generic)| {
799                        let field_ident = syn::Ident::new(name, Span::call_site());
800                        quote! { pub #field_ident: #generic }
801                    })
802                    .collect();
803
804                let struct_generics: Vec<proc_macro2::TokenStream> = loc_net_outputs
805                    .iter()
806                    .zip(net_out_generic_idents.iter())
807                    .map(|((_, is_tagged, ext_ty), generic)| {
808                        let payload = if let Some(ty) = ext_ty {
809                            quote! { #ty }
810                        } else {
811                            quote! { #root::runtime_support::dfir_rs::bytes::Bytes }
812                        };
813                        if *is_tagged {
814                            quote! { #generic: FnMut((#root::location::member_id::TaglessMemberId, #payload)) }
815                        } else {
816                            quote! { #generic: FnMut(#payload) }
817                        }
818                    })
819                    .collect();
820
821                for ((_, is_tagged, ext_ty), generic) in
822                    loc_net_outputs.iter().zip(net_out_generic_idents.iter())
823                {
824                    let payload = if let Some(ty) = ext_ty {
825                        quote! { #ty }
826                    } else {
827                        quote! { #root::runtime_support::dfir_rs::bytes::Bytes }
828                    };
829                    if *is_tagged {
830                        extra_fn_generics.push(
831                            quote! { #generic: FnMut((#root::location::member_id::TaglessMemberId, #payload)) + 'a },
832                        );
833                    } else {
834                        extra_fn_generics.push(quote! { #generic: FnMut(#payload) + 'a });
835                    }
836                }
837
838                net_out_params.push(quote! {
839                    __network_out: &'a mut #fn_ident::#net_out_struct_ident<#(#net_out_generic_idents),*>
840                });
841
842                for (name, _, _) in &loc_net_outputs {
843                    let field_ident = syn::Ident::new(name, Span::call_site());
844                    let var_ident =
845                        syn::Ident::new(&format!("__network_out_{name}"), Span::call_site());
846                    extra_destructure
847                        .push(quote! { let mut #var_ident = &mut __network_out.#field_ident; });
848                }
849
850                mod_items.push(quote! {
851                    pub struct #net_out_struct_ident<#(#struct_generics),*> {
852                        #(#struct_fields),*
853                    }
854                });
855            }
856
857            // Network inputs (Stream sources).
858            if let Some(mut loc_net_inputs) = env.network_inputs.remove(location_key) {
859                loc_net_inputs.sort_by(|a, b| a.0.cmp(&b.0));
860
861                let net_in_struct_ident = syn::Ident::new("EmbeddedNetworkIn", Span::call_site());
862
863                let net_in_generic_idents: Vec<syn::Ident> = loc_net_inputs
864                    .iter()
865                    .enumerate()
866                    .map(|(i, _)| quote::format_ident!("__NetIn{}", i))
867                    .collect();
868
869                let struct_fields: Vec<proc_macro2::TokenStream> = loc_net_inputs
870                    .iter()
871                    .zip(net_in_generic_idents.iter())
872                    .map(|((name, _, _), generic)| {
873                        let field_ident = syn::Ident::new(name, Span::call_site());
874                        quote! { pub #field_ident: #generic }
875                    })
876                    .collect();
877
878                let struct_generics: Vec<proc_macro2::TokenStream> = loc_net_inputs
879                    .iter()
880                    .zip(net_in_generic_idents.iter())
881                    .map(|((_, is_tagged, ext_ty), generic)| {
882                        match ext_ty {
883                            // Embedded (external) serialization: the raw payload is delivered
884                            // directly, with no transport `Result` wrapper.
885                            Some(ty) => {
886                                if *is_tagged {
887                                    quote! { #generic: __root_dfir_rs::futures::Stream<Item = (#root::location::member_id::TaglessMemberId, #ty)> + Unpin }
888                                } else {
889                                    quote! { #generic: __root_dfir_rs::futures::Stream<Item = #ty> + Unpin }
890                                }
891                            }
892                            None => {
893                                if *is_tagged {
894                                    quote! { #generic: __root_dfir_rs::futures::Stream<Item = Result<(#root::location::member_id::TaglessMemberId, __root_dfir_rs::bytes::BytesMut), std::io::Error>> + Unpin }
895                                } else {
896                                    quote! { #generic: __root_dfir_rs::futures::Stream<Item = Result<__root_dfir_rs::bytes::BytesMut, std::io::Error>> + Unpin }
897                                }
898                            }
899                        }
900                    })
901                    .collect();
902
903                for ((_, is_tagged, ext_ty), generic) in
904                    loc_net_inputs.iter().zip(net_in_generic_idents.iter())
905                {
906                    match ext_ty {
907                        Some(ty) => {
908                            if *is_tagged {
909                                extra_fn_generics.push(
910                                    quote! { #generic: __root_dfir_rs::futures::Stream<Item = (#root::location::member_id::TaglessMemberId, #ty)> + Unpin + 'a },
911                                );
912                            } else {
913                                extra_fn_generics.push(
914                                    quote! { #generic: __root_dfir_rs::futures::Stream<Item = #ty> + Unpin + 'a },
915                                );
916                            }
917                        }
918                        None => {
919                            if *is_tagged {
920                                extra_fn_generics.push(
921                                    quote! { #generic: __root_dfir_rs::futures::Stream<Item = Result<(#root::location::member_id::TaglessMemberId, __root_dfir_rs::bytes::BytesMut), std::io::Error>> + Unpin + 'a },
922                                );
923                            } else {
924                                extra_fn_generics.push(
925                                    quote! { #generic: __root_dfir_rs::futures::Stream<Item = Result<__root_dfir_rs::bytes::BytesMut, std::io::Error>> + Unpin + 'a },
926                                );
927                            }
928                        }
929                    }
930                }
931
932                net_in_params.push(quote! {
933                    __network_in: #fn_ident::#net_in_struct_ident<#(#net_in_generic_idents),*>
934                });
935
936                for (name, _, _) in &loc_net_inputs {
937                    let field_ident = syn::Ident::new(name, Span::call_site());
938                    let var_ident =
939                        syn::Ident::new(&format!("__network_in_{name}"), Span::call_site());
940                    extra_destructure.push(quote! { let #var_ident = __network_in.#field_ident; });
941                }
942
943                mod_items.push(quote! {
944                    pub struct #net_in_struct_ident<#(#struct_generics),*> {
945                        #(#struct_fields),*
946                    }
947                });
948            }
949
950            // Emit the module if there are any structs.
951            if !mod_items.is_empty() {
952                let output_mod: syn::Item = syn::parse_quote! {
953                    pub mod #fn_ident {
954                        use super::*;
955                        #(#mod_items)*
956                    }
957                };
958                items.push(output_mod);
959            }
960
961            // Build the function.
962            let all_params: Vec<proc_macro2::TokenStream> = cluster_params
963                .into_iter()
964                .chain(singleton_input_params)
965                .chain(input_params)
966                .chain(output_params)
967                .chain(net_in_params)
968                .chain(net_out_params)
969                .collect();
970
971            let ret_type: syn::Type = syn::parse_quote! { #root::runtime_support::dfir_rs::scheduled::context::Dfir<impl #root::runtime_support::dfir_rs::scheduled::context::TickClosure + 'a> };
972
973            let func = if !extra_fn_generics.is_empty() {
974                syn::parse_quote! {
975                    #[allow(unused, non_snake_case, clippy::suspicious_else_formatting)]
976                    pub fn #fn_ident<'a, #(#extra_fn_generics),*>(#(#all_params),*) -> #ret_type {
977                        #(#extra_destructure)*
978                        #dfir_tokens
979                    }
980                }
981            } else {
982                syn::parse_quote! {
983                    #[allow(unused, non_snake_case, clippy::suspicious_else_formatting)]
984                    pub fn #fn_ident<'a>(#(#all_params),*) -> #ret_type {
985                        #dfir_tokens
986                    }
987                }
988            };
989
990            items.push(func);
991        }
992
993        syn::parse_quote! {
994            use #orig_crate_name::__staged::__deps::*;
995            use #root::prelude::*;
996            use #root::runtime_support::dfir_rs as __root_dfir_rs;
997            pub use #orig_crate_name::__staged;
998
999            #( #items )*
1000        }
1001    }
1002}