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