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, wrapped in a
19//!   [`EmbeddedNetworkOutputChannel`](crate::compile::embedded_runtime::EmbeddedNetworkOutputChannel).
20//! - Sources (`EmbeddedNetworkIn`): one `Stream` field per incoming channel, wrapped in a
21//!   [`EmbeddedNetworkInputChannel`](crate::compile::embedded_runtime::EmbeddedNetworkInputChannel).
22//!
23//! Each location's module also has a marker type per channel end (`outgoing::*` and
24//! `incoming::*`) implementing
25//! [`EmbeddedNetworkChannel`](crate::compile::embedded_runtime::EmbeddedNetworkChannel), which
26//! describes the field's channel end: the guarantees of the stream there (the stream sent, or the
27//! one delivered) and the channel's network config, which the host agrees to.
28//!
29//! The exact field types depend on the channel's serialization:
30//! - With [`bincode`](crate::networking::Bincode) serialization, Hydro serializes to bytes, so
31//!   sinks are `FnMut(Bytes)` and sources are `Stream<Item = Result<BytesMut, io::Error>>`.
32//! - With [`embedded`](crate::networking::Embedded) serialization, the raw element type `T` flows
33//!   across the channel, so sinks are `FnMut(T)` and sources are `Stream<Item = T>` (with no
34//!   transport `Result` — the caller decides how to handle faults).
35//!
36//! Channels keyed by a cluster member (demux / cluster send) additionally carry a
37//! `TaglessMemberId` alongside the payload in these types.
38//!
39//! The caller is responsible for wiring these together (e.g. via in-memory channels,
40//! sockets, etc.). External ports are not supported.
41
42use std::future::Future;
43use std::io::Error;
44use std::pin::Pin;
45
46use bytes::{Bytes, BytesMut};
47use dfir_lang::diagnostic::Diagnostics;
48use dfir_lang::graph::{AsCodeOptions, DfirGraph};
49use futures::{Sink, Stream};
50use proc_macro2::Span;
51use quote::quote;
52use serde::Serialize;
53use serde::de::DeserializeOwned;
54use slotmap::SparseSecondaryMap;
55
56use super::deploy_provider::{ClusterSpec, Deploy, ExternalSpec, Node, ProcessSpec, RegisterPort};
57use crate::compile::builder::ExternalPortId;
58use crate::location::dynamic::LocationId;
59use crate::location::{LocationKey, NetworkHint};
60
61/// Marker type for the embedded deployment backend.
62///
63/// All networking methods panic — this backend only supports pure local computation.
64pub enum EmbeddedDeploy {}
65
66/// A trivial node type for embedded deployment. Stores a user-provided function name.
67#[derive(Clone)]
68pub struct EmbeddedNode {
69    /// The function name to use in the generated code for this location.
70    pub fn_name: String,
71    /// The location key for this node, used to register network ports.
72    pub location_key: LocationKey,
73}
74
75impl Node for EmbeddedNode {
76    type Port = ();
77    type Meta = ();
78    type InstantiateEnv = EmbeddedInstantiateEnv;
79
80    fn next_port(&self) -> Self::Port {}
81
82    fn update_meta(&self, _meta: &Self::Meta) {}
83
84    fn instantiate(
85        &self,
86        _env: &mut Self::InstantiateEnv,
87        _meta: &mut Self::Meta,
88        _graph: DfirGraph,
89        _extra_stmts: &[syn::Stmt],
90        _sidecars: &[syn::Expr],
91        _as_code_options: &AsCodeOptions,
92    ) {
93        // No-op: embedded mode doesn't instantiate nodes at deploy time.
94    }
95}
96
97impl<'a> RegisterPort<'a, EmbeddedDeploy> for EmbeddedNode {
98    fn register(&self, _external_port_id: ExternalPortId, _port: Self::Port) {
99        panic!("EmbeddedDeploy does not support external ports");
100    }
101
102    #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
103    fn as_bytes_bidi(
104        &self,
105        _external_port_id: ExternalPortId,
106    ) -> impl Future<
107        Output = super::deploy_provider::DynSourceSink<Result<BytesMut, Error>, Bytes, Error>,
108    > + 'a {
109        async { panic!("EmbeddedDeploy does not support external ports") }
110    }
111
112    #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
113    fn as_bincode_bidi<InT, OutT>(
114        &self,
115        _external_port_id: ExternalPortId,
116    ) -> impl Future<Output = super::deploy_provider::DynSourceSink<OutT, InT, Error>> + 'a
117    where
118        InT: Serialize + 'static,
119        OutT: DeserializeOwned + 'static,
120    {
121        async { panic!("EmbeddedDeploy does not support external ports") }
122    }
123
124    #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
125    fn as_bincode_sink<T>(
126        &self,
127        _external_port_id: ExternalPortId,
128    ) -> impl Future<Output = Pin<Box<dyn Sink<T, Error = Error>>>> + 'a
129    where
130        T: Serialize + 'static,
131    {
132        async { panic!("EmbeddedDeploy does not support external ports") }
133    }
134
135    #[expect(clippy::manual_async_fn, reason = "false positive, involves lifetimes")]
136    fn as_bincode_source<T>(
137        &self,
138        _external_port_id: ExternalPortId,
139    ) -> impl Future<Output = Pin<Box<dyn Stream<Item = T>>>> + 'a
140    where
141        T: DeserializeOwned + 'static,
142    {
143        async { panic!("EmbeddedDeploy does not support external ports") }
144    }
145}
146
147impl<S: Into<String>> ProcessSpec<'_, EmbeddedDeploy> for S {
148    fn build(self, location_key: LocationKey, _name_hint: &str) -> EmbeddedNode {
149        EmbeddedNode {
150            fn_name: self.into(),
151            location_key,
152        }
153    }
154}
155
156impl<S: Into<String>> ClusterSpec<'_, EmbeddedDeploy> for S {
157    fn build(self, location_key: LocationKey, _name_hint: &str) -> EmbeddedNode {
158        EmbeddedNode {
159            fn_name: self.into(),
160            location_key,
161        }
162    }
163}
164
165impl<S: Into<String>> ExternalSpec<'_, EmbeddedDeploy> for S {
166    fn build(self, location_key: LocationKey, _name_hint: &str) -> EmbeddedNode {
167        EmbeddedNode {
168            fn_name: self.into(),
169            location_key,
170        }
171    }
172}
173
174/// One end of a named network channel at a location.
175#[derive(Clone)]
176struct EmbeddedNetworkPort {
177    name: String,
178    /// Whether each element is paired with a `TaglessMemberId` (its destination or sender).
179    is_tagged: bool,
180    /// The raw payload type of an unserialized `EMBEDDED` channel; `None` for bincode.
181    external_type: Option<syn::Type>,
182    /// The location at the other end.
183    peer: LocationKey,
184    /// The channel config's transport and serialization types, and the config written out.
185    transport: syn::Type,
186    serialization: syn::Type,
187    config: String,
188}
189
190impl EmbeddedNetworkPort {
191    fn new(
192        name: &str,
193        is_tagged: bool,
194        external_type: Option<&syn::Type>,
195        peer: LocationKey,
196        networking_info: &crate::networking::NetworkingInfo,
197    ) -> Self {
198        let (transport, serialization, config) =
199            channel_config_types(networking_info, external_type.is_some());
200        EmbeddedNetworkPort {
201            name: name.to_owned(),
202            is_tagged,
203            external_type: external_type.cloned(),
204            peer,
205            transport,
206            serialization,
207            config,
208        }
209    }
210}
211
212/// The `Tr` and `S` types of the network config that `networking_info` describes, as the host's
213/// `ChannelConfiguration` names them, and that config written out.
214fn channel_config_types(
215    networking_info: &crate::networking::NetworkingInfo,
216    external: bool,
217) -> (syn::Type, syn::Type, String) {
218    use crate::compile::ir::{StreamOrder, StreamRetry};
219    use crate::networking::{EmbeddedFault, NetworkingInfo, TcpFault, UdpFault};
220    let root = crate::staging_util::get_this_crate();
221    let fault = |name: &str| -> syn::Type {
222        let ident = syn::Ident::new(name, Span::call_site());
223        syn::parse_quote!(#root::networking::#ident)
224    };
225    let transport: syn::Type = match networking_info {
226        NetworkingInfo::Tcp { fault: f } => {
227            let f = fault(match f {
228                TcpFault::FailStop => "FailStop",
229                TcpFault::Lossy => "Lossy",
230                TcpFault::LossyDelayedForever => "LossyDelayedForever",
231            });
232            syn::parse_quote!(#root::networking::Tcp<#f>)
233        }
234        NetworkingInfo::Udp { fault: f } => {
235            let f = fault(match f {
236                UdpFault::Lossy => "Lossy",
237                UdpFault::LossyDelayedForever => "LossyDelayedForever",
238            });
239            syn::parse_quote!(#root::networking::Udp<#f>)
240        }
241        NetworkingInfo::Embedded {
242            retries,
243            ordering,
244            fault: f,
245        } => {
246            let retries: syn::Type = match retries {
247                StreamRetry::ExactlyOnce => {
248                    syn::parse_quote!(#root::live_collections::stream::ExactlyOnce)
249                }
250                StreamRetry::AtLeastOnce => {
251                    syn::parse_quote!(#root::live_collections::stream::AtLeastOnce)
252                }
253            };
254            let ordering: syn::Type = match ordering {
255                StreamOrder::TotalOrder => {
256                    syn::parse_quote!(#root::live_collections::stream::TotalOrder)
257                }
258                StreamOrder::NoOrder => syn::parse_quote!(#root::live_collections::stream::NoOrder),
259            };
260            let f = fault(match f {
261                EmbeddedFault::FailStop => "FailStop",
262                EmbeddedFault::Lossy => "Lossy",
263                EmbeddedFault::LossyDelayedForever => "LossyDelayedForever",
264            });
265            syn::parse_quote!(#root::networking::EmbeddedTransport<#retries, #ordering, #f>)
266        }
267    };
268    let serialization: syn::Type = if external {
269        syn::parse_quote!(#root::networking::Embedded)
270    } else {
271        syn::parse_quote!(#root::networking::Bincode)
272    };
273    (
274        transport,
275        serialization,
276        config_doc(networking_info, external),
277    )
278}
279
280/// The network config that `networking_info` describes, written out for the docs.
281fn config_doc(networking_info: &crate::networking::NetworkingInfo, external: bool) -> String {
282    use crate::compile::ir::{StreamOrder, StreamRetry};
283    use crate::networking::{EmbeddedFault, NetworkingInfo, TcpFault, UdpFault};
284    let serialization = if external { "" } else { ".bincode()" };
285    match networking_info {
286        NetworkingInfo::Tcp { fault } => {
287            let fault = match fault {
288                TcpFault::FailStop => ".fail_stop()",
289                TcpFault::Lossy => ".lossy(nondet)",
290                TcpFault::LossyDelayedForever => ".lossy_delayed_forever()",
291            };
292            format!("TCP{fault}{serialization}")
293        }
294        NetworkingInfo::Udp { fault } => {
295            let fault = match fault {
296                UdpFault::Lossy => ".lossy(nondet)",
297                UdpFault::LossyDelayedForever => ".lossy_delayed_forever()",
298            };
299            format!("UDP{fault}{serialization}")
300        }
301        NetworkingInfo::Embedded {
302            retries,
303            ordering,
304            fault,
305        } => {
306            // `EMBEDDED` with a `never_*` or `may_*` declaration for each anomaly.
307            let reorder = match ordering {
308                StreamOrder::TotalOrder => ".never_reorders()",
309                StreamOrder::NoOrder => ".may_reorder()",
310            };
311            let duplicate = match retries {
312                StreamRetry::ExactlyOnce => ".never_duplicates()",
313                StreamRetry::AtLeastOnce => ".may_duplicate()",
314            };
315            let loss = match fault {
316                EmbeddedFault::FailStop => ".never_drops()",
317                EmbeddedFault::Lossy => ".may_drop(nondet)",
318                EmbeddedFault::LossyDelayedForever => ".may_delay_forever()",
319            };
320            format!("EMBEDDED{reorder}{duplicate}{loss}")
321        }
322    }
323}
324
325/// A stream input or output, by its field name and element type.
326#[derive(Clone)]
327struct EmbeddedStreamPort {
328    ident: syn::Ident,
329    element_type: syn::Type,
330}
331
332/// Collected embedded input/output registrations, keyed by location.
333///
334/// During `compile_network`, each `HydroSource::Embedded` and `HydroRoot::EmbeddedOutput`
335/// IR node registers its ident, element type, and location key here.
336/// `generate_embedded` then uses this to add the appropriate parameters
337/// to each generated function.
338#[derive(Default)]
339pub struct EmbeddedInstantiateEnv {
340    inputs: SparseSecondaryMap<LocationKey, Vec<EmbeddedStreamPort>>,
341    singleton_inputs: SparseSecondaryMap<LocationKey, Vec<(syn::Ident, syn::Type)>>,
342    outputs: SparseSecondaryMap<LocationKey, Vec<EmbeddedStreamPort>>,
343    /// Sending ends of network channels.
344    network_outputs: SparseSecondaryMap<LocationKey, Vec<EmbeddedNetworkPort>>,
345    /// Receiving ends of network channels.
346    network_inputs: SparseSecondaryMap<LocationKey, Vec<EmbeddedNetworkPort>>,
347    /// The clusters whose membership each location needs.
348    membership_streams: SparseSecondaryMap<LocationKey, Vec<LocationKey>>,
349    boundary: BoundaryGuarantees,
350}
351
352/// The ordering and retries of a stream crossing the boundary.
353#[derive(Clone, Debug)]
354struct EmbeddedStreamGuarantees {
355    order: crate::compile::ir::StreamOrder,
356    retry: crate::compile::ir::StreamRetry,
357}
358
359impl EmbeddedStreamGuarantees {
360    fn of(metadata: &crate::compile::ir::HydroIrMetadata, what: &str) -> Self {
361        use crate::compile::ir::CollectionKind;
362        match &metadata.collection_kind {
363            CollectionKind::Stream { order, retry, .. }
364            | CollectionKind::KeyedStream {
365                value_order: order,
366                value_retry: retry,
367                ..
368            } => EmbeddedStreamGuarantees {
369                order: order.clone(),
370                retry: retry.clone(),
371            },
372            kind => panic!("{what} must be a stream or keyed stream, not {kind:?}"),
373        }
374    }
375}
376
377/// The guarantees of the stream sent on a network channel and of the stream it delivers.
378#[derive(Clone, Debug)]
379struct EmbeddedNetworkGuarantees {
380    sent: EmbeddedStreamGuarantees,
381    received: EmbeddedStreamGuarantees,
382}
383
384/// The guarantees of every stream crossing the boundary, read from the IR before compiling: only
385/// the embedded backend needs them, so they do not go through [`Deploy`].
386#[derive(Default)]
387struct BoundaryGuarantees {
388    inputs: std::collections::HashMap<(LocationKey, String), EmbeddedStreamGuarantees>,
389    outputs: std::collections::HashMap<(LocationKey, String), EmbeddedStreamGuarantees>,
390    networks: std::collections::HashMap<String, EmbeddedNetworkGuarantees>,
391}
392
393impl BoundaryGuarantees {
394    fn collect(ir: &mut [crate::compile::ir::HydroRoot]) -> Self {
395        use crate::compile::ir::{HydroNode, HydroRoot, HydroSource};
396        let location_key = |id: &LocationId, what: &str| match id.root() {
397            LocationId::Process(key) | LocationId::Cluster(key) => *key,
398            _ => panic!("{what} must be on a process or cluster"),
399        };
400        let mut boundary = BoundaryGuarantees::default();
401        let cell = std::cell::RefCell::new(&mut boundary);
402        crate::compile::ir::transform_bottom_up(
403            ir,
404            &mut |root| {
405                if let HydroRoot::EmbeddedOutput { ident, input, .. } = root {
406                    let what = "an embedded output";
407                    cell.borrow_mut().outputs.insert(
408                        (
409                            location_key(&input.metadata().location_id, what),
410                            ident.to_string(),
411                        ),
412                        EmbeddedStreamGuarantees::of(input.metadata(), what),
413                    );
414                }
415            },
416            &mut |node| match node {
417                HydroNode::Source {
418                    source: HydroSource::Embedded(ident),
419                    metadata,
420                } => {
421                    let what = "an embedded input";
422                    cell.borrow_mut().inputs.insert(
423                        (location_key(&metadata.location_id, what), ident.to_string()),
424                        EmbeddedStreamGuarantees::of(metadata, what),
425                    );
426                }
427                HydroNode::Network {
428                    name: Some(name),
429                    input,
430                    metadata,
431                    ..
432                } => {
433                    let what = "a network channel's stream";
434                    cell.borrow_mut().networks.insert(
435                        name.clone(),
436                        EmbeddedNetworkGuarantees {
437                            sent: EmbeddedStreamGuarantees::of(input.metadata(), what),
438                            received: EmbeddedStreamGuarantees::of(metadata, what),
439                        },
440                    );
441                }
442                _ => {}
443            },
444            false,
445        );
446        boundary
447    }
448
449    fn input(&self, location_key: LocationKey, ident: &syn::Ident) -> EmbeddedStreamGuarantees {
450        self.inputs[&(location_key, ident.to_string())].clone()
451    }
452
453    fn output(&self, location_key: LocationKey, ident: &syn::Ident) -> EmbeddedStreamGuarantees {
454        self.outputs[&(location_key, ident.to_string())].clone()
455    }
456
457    fn network(&self, name: &str) -> EmbeddedNetworkGuarantees {
458        self.networks[name].clone()
459    }
460}
461
462impl<'a> Deploy<'a> for EmbeddedDeploy {
463    type Meta = ();
464    type InstantiateEnv = EmbeddedInstantiateEnv;
465
466    type Process = EmbeddedNode;
467    type Cluster = EmbeddedNode;
468    type External = EmbeddedNode;
469
470    const SUPPORTS_EXTERNAL_SERIALIZATION: bool = true;
471
472    fn o2o_sink_source(
473        env: &mut Self::InstantiateEnv,
474        p1: &Self::Process,
475        _p1_port: &(),
476        p2: &Self::Process,
477        _p2_port: &(),
478        name: Option<&str>,
479        networking_info: &crate::networking::NetworkingInfo,
480        external_types: Option<(&syn::Type, &syn::Type)>,
481    ) -> (syn::Expr, syn::Expr) {
482        let name = name.expect(
483            "EmbeddedDeploy o2o networking requires a channel name. Use `TCP.name(\"my_channel\")` to provide one.",
484        );
485
486        let sink_ident = syn::Ident::new(&format!("__network_out_{name}"), Span::call_site());
487        let source_ident = syn::Ident::new(&format!("__network_in_{name}"), Span::call_site());
488
489        env.network_outputs
490            .entry(p1.location_key)
491            .unwrap()
492            .or_default()
493            .push(EmbeddedNetworkPort::new(
494                name,
495                false,
496                external_types.map(|(i, _)| i),
497                p2.location_key,
498                networking_info,
499            ));
500        env.network_inputs
501            .entry(p2.location_key)
502            .unwrap()
503            .or_default()
504            .push(EmbeddedNetworkPort::new(
505                name,
506                false,
507                external_types.map(|(_, o)| o),
508                p1.location_key,
509                networking_info,
510            ));
511
512        (
513            syn::parse_quote!(__root_dfir_rs::sinktools::for_each(#sink_ident)),
514            syn::parse_quote!(#source_ident),
515        )
516    }
517
518    fn o2o_connect(
519        _p1: &Self::Process,
520        _p1_port: &(),
521        _p2: &Self::Process,
522        _p2_port: &(),
523    ) -> Box<dyn FnOnce()> {
524        Box::new(|| {})
525    }
526
527    fn o2m_sink_source(
528        env: &mut Self::InstantiateEnv,
529        p1: &Self::Process,
530        _p1_port: &(),
531        c2: &Self::Cluster,
532        _c2_port: &(),
533        name: Option<&str>,
534        networking_info: &crate::networking::NetworkingInfo,
535        external_types: Option<(&syn::Type, &syn::Type)>,
536    ) -> (syn::Expr, syn::Expr) {
537        let name = name.expect("EmbeddedDeploy o2m networking requires a channel name.");
538        let sink_ident = syn::Ident::new(&format!("__network_out_{name}"), Span::call_site());
539        let source_ident = syn::Ident::new(&format!("__network_in_{name}"), Span::call_site());
540        env.network_outputs
541            .entry(p1.location_key)
542            .unwrap()
543            .or_default()
544            .push(EmbeddedNetworkPort::new(
545                name,
546                true,
547                external_types.map(|(i, _)| i),
548                c2.location_key,
549                networking_info,
550            ));
551        env.network_inputs
552            .entry(c2.location_key)
553            .unwrap()
554            .or_default()
555            .push(EmbeddedNetworkPort::new(
556                name,
557                false,
558                external_types.map(|(_, o)| o),
559                p1.location_key,
560                networking_info,
561            ));
562        (
563            syn::parse_quote!(__root_dfir_rs::sinktools::for_each(#sink_ident)),
564            syn::parse_quote!(#source_ident),
565        )
566    }
567
568    fn o2m_connect(
569        _p1: &Self::Process,
570        _p1_port: &(),
571        _c2: &Self::Cluster,
572        _c2_port: &(),
573    ) -> Box<dyn FnOnce()> {
574        Box::new(|| {})
575    }
576
577    fn m2o_sink_source(
578        env: &mut Self::InstantiateEnv,
579        c1: &Self::Cluster,
580        _c1_port: &(),
581        p2: &Self::Process,
582        _p2_port: &(),
583        name: Option<&str>,
584        networking_info: &crate::networking::NetworkingInfo,
585        external_types: Option<(&syn::Type, &syn::Type)>,
586    ) -> (syn::Expr, syn::Expr) {
587        let name = name.expect("EmbeddedDeploy m2o networking requires a channel name.");
588        let sink_ident = syn::Ident::new(&format!("__network_out_{name}"), Span::call_site());
589        let source_ident = syn::Ident::new(&format!("__network_in_{name}"), Span::call_site());
590        env.network_outputs
591            .entry(c1.location_key)
592            .unwrap()
593            .or_default()
594            .push(EmbeddedNetworkPort::new(
595                name,
596                false,
597                external_types.map(|(i, _)| i),
598                p2.location_key,
599                networking_info,
600            ));
601        env.network_inputs
602            .entry(p2.location_key)
603            .unwrap()
604            .or_default()
605            .push(EmbeddedNetworkPort::new(
606                name,
607                true,
608                external_types.map(|(_, o)| o),
609                c1.location_key,
610                networking_info,
611            ));
612        (
613            syn::parse_quote!(__root_dfir_rs::sinktools::for_each(#sink_ident)),
614            syn::parse_quote!(#source_ident),
615        )
616    }
617
618    fn m2o_connect(
619        _c1: &Self::Cluster,
620        _c1_port: &(),
621        _p2: &Self::Process,
622        _p2_port: &(),
623    ) -> Box<dyn FnOnce()> {
624        Box::new(|| {})
625    }
626
627    fn m2m_sink_source(
628        env: &mut Self::InstantiateEnv,
629        c1: &Self::Cluster,
630        _c1_port: &(),
631        c2: &Self::Cluster,
632        _c2_port: &(),
633        name: Option<&str>,
634        networking_info: &crate::networking::NetworkingInfo,
635        external_types: Option<(&syn::Type, &syn::Type)>,
636    ) -> (syn::Expr, syn::Expr) {
637        let name = name.expect("EmbeddedDeploy m2m networking requires a channel name.");
638        let sink_ident = syn::Ident::new(&format!("__network_out_{name}"), Span::call_site());
639        let source_ident = syn::Ident::new(&format!("__network_in_{name}"), Span::call_site());
640        env.network_outputs
641            .entry(c1.location_key)
642            .unwrap()
643            .or_default()
644            .push(EmbeddedNetworkPort::new(
645                name,
646                true,
647                external_types.map(|(i, _)| i),
648                c2.location_key,
649                networking_info,
650            ));
651        env.network_inputs
652            .entry(c2.location_key)
653            .unwrap()
654            .or_default()
655            .push(EmbeddedNetworkPort::new(
656                name,
657                true,
658                external_types.map(|(_, o)| o),
659                c1.location_key,
660                networking_info,
661            ));
662        (
663            syn::parse_quote!(__root_dfir_rs::sinktools::for_each(#sink_ident)),
664            syn::parse_quote!(#source_ident),
665        )
666    }
667
668    fn m2m_connect(
669        _c1: &Self::Cluster,
670        _c1_port: &(),
671        _c2: &Self::Cluster,
672        _c2_port: &(),
673    ) -> Box<dyn FnOnce()> {
674        Box::new(|| {})
675    }
676
677    fn e2o_many_source(
678        _extra_stmts: &mut Vec<syn::Stmt>,
679        _p2: &Self::Process,
680        _p2_port: &(),
681        _codec_type: &syn::Type,
682        _shared_handle: String,
683    ) -> syn::Expr {
684        panic!("EmbeddedDeploy does not support networking (e2o)")
685    }
686
687    fn e2o_many_sink(_shared_handle: String) -> syn::Expr {
688        panic!("EmbeddedDeploy does not support networking (e2o)")
689    }
690
691    fn e2o_source(
692        _extra_stmts: &mut Vec<syn::Stmt>,
693        _p1: &Self::External,
694        _p1_port: &(),
695        _p2: &Self::Process,
696        _p2_port: &(),
697        _codec_type: &syn::Type,
698        _shared_handle: String,
699    ) -> syn::Expr {
700        panic!("EmbeddedDeploy does not support networking (e2o)")
701    }
702
703    fn e2o_connect(
704        _p1: &Self::External,
705        _p1_port: &(),
706        _p2: &Self::Process,
707        _p2_port: &(),
708        _many: bool,
709        _server_hint: NetworkHint,
710    ) -> Box<dyn FnOnce()> {
711        panic!("EmbeddedDeploy does not support networking (e2o)")
712    }
713
714    fn o2e_sink(
715        _p1: &Self::Process,
716        _p1_port: &(),
717        _p2: &Self::External,
718        _p2_port: &(),
719        _shared_handle: String,
720    ) -> syn::Expr {
721        panic!("EmbeddedDeploy does not support networking (o2e)")
722    }
723
724    fn cluster_ids(_of_cluster: LocationKey) -> syn::Expr {
725        panic!("EmbeddedDeploy does not support cluster IDs");
726    }
727
728    fn cluster_self_id() -> syn::Expr {
729        syn::parse_quote!(__cluster_self_id)
730    }
731
732    fn cluster_membership_stream(
733        env: &mut Self::InstantiateEnv,
734        at_location: &LocationId,
735        location_id: &LocationId,
736    ) -> syn::Expr {
737        let at_key = match at_location {
738            LocationId::Process(key) | LocationId::Cluster(key) => *key,
739            _ => panic!("cluster_membership_stream must be called from a process or cluster"),
740        };
741        let cluster_key = match location_id {
742            LocationId::Cluster(key) => *key,
743            _ => panic!("cluster_membership_stream target must be a cluster"),
744        };
745        let vec = env.membership_streams.entry(at_key).unwrap().or_default();
746        let idx = if let Some(pos) = vec.iter().position(|k| *k == cluster_key) {
747            pos
748        } else {
749            vec.push(cluster_key);
750            vec.len() - 1
751        };
752
753        let ident = quote::format_ident!("__membership_{}", idx);
754        syn::parse_quote!(#ident)
755    }
756
757    fn register_embedded_stream_input(
758        env: &mut Self::InstantiateEnv,
759        location_key: LocationKey,
760        ident: &syn::Ident,
761        element_type: &syn::Type,
762    ) {
763        env.inputs
764            .entry(location_key)
765            .unwrap()
766            .or_default()
767            .push(EmbeddedStreamPort {
768                ident: ident.clone(),
769                element_type: element_type.clone(),
770            });
771    }
772
773    fn register_embedded_singleton_input(
774        env: &mut Self::InstantiateEnv,
775        location_key: LocationKey,
776        ident: &syn::Ident,
777        element_type: &syn::Type,
778    ) {
779        env.singleton_inputs
780            .entry(location_key)
781            .unwrap()
782            .or_default()
783            .push((ident.clone(), element_type.clone()));
784    }
785
786    fn register_embedded_output(
787        env: &mut Self::InstantiateEnv,
788        location_key: LocationKey,
789        ident: &syn::Ident,
790        element_type: &syn::Type,
791    ) {
792        env.outputs
793            .entry(location_key)
794            .unwrap()
795            .or_default()
796            .push(EmbeddedStreamPort {
797                ident: ident.clone(),
798                element_type: element_type.clone(),
799            });
800    }
801}
802
803/// The generated pieces for one direction of a location's network channels: the
804/// `EmbeddedNetworkIn` or `EmbeddedNetworkOut` struct, its module of marker types, and the
805/// function parameter. Both directions are generated the same way, one piece per port.
806struct NetworkPortTokens {
807    fields: Vec<syn::Ident>,
808    field_docs: Vec<String>,
809    /// Each field's type: `EmbeddedNetworkInputChannel<Marker, S>` (receiving) or `EmbeddedNetworkOutputChannel<Marker, F>`
810    /// (sending), for the port's marker type (which carries its stream's guarantees and the
811    /// channel's configuration) and generic parameter.
812    field_types: Vec<proc_macro2::TokenStream>,
813    generics: Vec<syn::Ident>,
814    items: Vec<proc_macro2::TokenStream>,
815    marker_defs: Vec<proc_macro2::TokenStream>,
816}
817
818impl NetworkPortTokens {
819    fn new(
820        ports: &[EmbeddedNetworkPort],
821        boundary: &BoundaryGuarantees,
822        fn_names: &SparseSecondaryMap<LocationKey, &str>,
823        generic_prefix: &str,
824        sending: bool,
825        item: impl Fn(&EmbeddedNetworkPort) -> proc_macro2::TokenStream,
826    ) -> Self {
827        let root = crate::staging_util::get_this_crate();
828        let items: Vec<proc_macro2::TokenStream> = ports.iter().map(&item).collect();
829        // Each port's field, which its marker type is named after.
830        let fields: Vec<syn::Ident> = ports
831            .iter()
832            .map(|port| syn::Ident::new(&port.name, Span::call_site()))
833            .collect();
834        let mut marker_defs = Vec::with_capacity(ports.len());
835        let mut field_docs = Vec::with_capacity(ports.len());
836        for (port, marker) in ports.iter().zip(&fields) {
837            let name = &port.name;
838            let peer = fn_names[port.peer];
839            let channel = boundary.network(name);
840            let guarantees = if sending {
841                &channel.sent
842            } else {
843                &channel.received
844            };
845            let doc = format!(
846                "The `{name}` channel's end at this location; its other end is at `{peer}`.\n\n{}",
847                network_end_doc(port, guarantees, sending)
848            );
849            let marker_def = marker_def(&root, marker, &doc, guarantees);
850            let (transport, serialization) = (&port.transport, &port.serialization);
851            marker_defs.push(quote! {
852                #marker_def
853
854                impl #root::compile::embedded_runtime::EmbeddedNetworkChannel for #marker {
855                    type Transport = #transport;
856                    type Serialization = #serialization;
857                }
858            });
859            let (action, module) = if sending {
860                (format!("Sends on `{name}` to `{peer}`"), "outgoing")
861            } else {
862                (format!("Receives `{name}` from `{peer}`"), "incoming")
863            };
864            field_docs.push(format!("{action} (marker type: [`{module}::{marker}`])."));
865        }
866        let generics: Vec<syn::Ident> = (0..ports.len())
867            .map(|i| quote::format_ident!("{}{}", generic_prefix, i))
868            .collect();
869        let module = syn::Ident::new(
870            if sending { "outgoing" } else { "incoming" },
871            Span::call_site(),
872        );
873        // The host acknowledges the channel's configuration at both ends. At the sending end it
874        // acknowledges the stream sent, as for an output; at the receiving end it promises the
875        // stream delivered, as for an input.
876        let wrapper = if sending {
877            quote! { #root::compile::embedded_runtime::EmbeddedNetworkOutputChannel }
878        } else {
879            quote! { #root::compile::embedded_runtime::EmbeddedNetworkInputChannel }
880        };
881        let field_types = fields
882            .iter()
883            .zip(&generics)
884            .map(|(marker, generic)| quote! { #wrapper<#module::#marker, #generic> })
885            .collect();
886        NetworkPortTokens {
887            fields,
888            field_docs,
889            field_types,
890            generics,
891            items,
892            marker_defs,
893        }
894    }
895}
896
897/// The marker type of a field crossing the boundary, implementing `EmbeddedChannel` with its
898/// `guarantees`. Channels are named by strings, so each field gets a type, named after it, for
899/// the type checker to check the host's side against.
900fn marker_def(
901    root: &proc_macro2::TokenStream,
902    marker: &syn::Ident,
903    doc: &str,
904    guarantees: &EmbeddedStreamGuarantees,
905) -> proc_macro2::TokenStream {
906    let ordering = guarantees.order.type_tokens();
907    let retries = guarantees.retry.type_tokens();
908    quote! {
909        #[doc = #doc]
910        #[allow(non_camel_case_types, reason = "named after its field")]
911        pub struct #marker;
912
913        impl #root::compile::embedded_runtime::EmbeddedChannel for #marker {
914            type Ordering = #ordering;
915            type Retries = #retries;
916        }
917    }
918}
919
920/// How a host promises `guarantees` for a stream flowing into the dataflow, in words.
921fn promise_doc(guarantees: &EmbeddedStreamGuarantees, wrapper: &str) -> String {
922    format!(
923        "Promise its guarantees with `{wrapper}::new(.., Promises::new(){})`.",
924        promises_chain(guarantees)
925    )
926}
927
928/// The `Promises` builder calls that promise exactly `guarantees`.
929fn promises_chain(guarantees: &EmbeddedStreamGuarantees) -> String {
930    use crate::compile::ir::{StreamOrder, StreamRetry};
931    [
932        (guarantees.order == StreamOrder::TotalOrder).then_some(".total_order(proof)"),
933        (guarantees.retry == StreamRetry::ExactlyOnce).then_some(".exactly_once(proof)"),
934    ]
935    .into_iter()
936    .flatten()
937    .collect()
938}
939
940/// How a host acknowledges what an output with `guarantees` may do, in words.
941fn acknowledge_doc(guarantees: &EmbeddedStreamGuarantees) -> String {
942    format!(
943        "Acknowledge what it may do with \
944         `EmbeddedOutputChannel::new(.., Acknowledgements::new(){})`.",
945        acknowledgements_chain(guarantees)
946    )
947}
948
949/// The `Acknowledgements` calls for what a stream with `guarantees` may do.
950fn acknowledgements_chain(guarantees: &EmbeddedStreamGuarantees) -> String {
951    use crate::compile::ir::{StreamOrder, StreamRetry};
952    [
953        (guarantees.order == StreamOrder::NoOrder).then_some(".no_order()"),
954        (guarantees.retry == StreamRetry::AtLeastOnce).then_some(".at_least_once()"),
955    ]
956    .into_iter()
957    .flatten()
958    .collect()
959}
960
961/// How the host builds the channel end `port`, for its marker's docs.
962fn network_end_doc(
963    port: &EmbeddedNetworkPort,
964    guarantees: &EmbeddedStreamGuarantees,
965    sending: bool,
966) -> String {
967    let config = &port.config;
968    if sending {
969        format!(
970            "Acknowledge the channel's configuration and what the stream sent on it may do with \
971             `EmbeddedNetworkOutputChannel::new(.., ChannelConfiguration::new({config}), \
972             Acknowledgements::new(){})`.",
973            acknowledgements_chain(guarantees)
974        )
975    } else {
976        format!(
977            "Acknowledge the channel's configuration and promise the guarantees of the stream it \
978             delivers with `EmbeddedNetworkInputChannel::new(.., ChannelConfiguration::new({config}), \
979             Promises::new(){})`.",
980            promises_chain(guarantees)
981        )
982    }
983}
984
985/// Options controlling the Rust source emitted by
986/// [`DeployFlow::generate_embedded_with_options`](super::deploy::DeployFlow::generate_embedded_with_options).
987///
988/// This is deliberately *not* the full set of DFIR codegen knobs ([`AsCodeOptions`]). Most of
989/// those are decided by Hydro on the user's behalf: type guards are excluded because Hydro has
990/// already type-checked the dataflow and injects its own type hints, the serialized meta graph is
991/// excluded because nothing consumes it in embedded mode, and runtime metrics tracking is driven by
992/// [sidecars](crate::telemetry::Sidecar) rather than a raw flag. Only the choices that genuinely
993/// belong to the embedding developer are exposed here, which also keeps the embedded API stable
994/// as the underlying DFIR options evolve.
995#[derive(Clone, Debug, Default)]
996#[non_exhaustive]
997pub struct EmbeddedCodegenOptions {
998    /// Emit a named wrapper function around every DFIR operator so that each operator gets its own
999    /// stack frame in profiles/flamegraphs.
1000    ///
1001    /// Off by default: the wrappers are purely for profiler attribution (Hydro already type-checks
1002    /// the dataflow), and they make the generated code several times more verbose. Enable this
1003    /// when profiling an embedded Hydro program.
1004    pub require_flame_attribution: bool,
1005}
1006
1007impl EmbeddedCodegenOptions {
1008    /// Returns options with [`Self::require_flame_attribution`] set.
1009    pub fn with_flame_attribution() -> Self {
1010        Self {
1011            require_flame_attribution: true,
1012        }
1013    }
1014}
1015
1016impl super::deploy::DeployFlow<'_, EmbeddedDeploy> {
1017    /// Generates a `syn::File` containing one function per location in the flow.
1018    ///
1019    /// Each generated function has the signature:
1020    /// ```ignore
1021    /// pub fn <fn_name>() -> dfir_rs::scheduled::graph::Dfir<'static>
1022    /// ```
1023    /// where `fn_name` is the `String` passed to `with_process` / `with_cluster`.
1024    ///
1025    /// The returned `Dfir` can be manually executed by the caller.
1026    ///
1027    /// Uses the default [`EmbeddedCodegenOptions`]; see
1028    /// [`Self::generate_embedded_with_options`] to customize them.
1029    ///
1030    /// # Arguments
1031    ///
1032    /// * `crate_name` — the name of the crate containing the Hydro program (used for stageleft
1033    ///   re-exports). Hyphens will be replaced with underscores.
1034    ///
1035    /// # Usage
1036    ///
1037    /// Typically called from a `build.rs` in a wrapper crate:
1038    /// ```ignore
1039    /// // build.rs
1040    /// let deploy = flow.with_process(&process, "my_fn".to_string());
1041    /// let code = deploy.generate_embedded("my_hydro_crate");
1042    /// let out_dir = std::env::var("OUT_DIR").unwrap();
1043    /// std::fs::write(format!("{out_dir}/embedded.rs"), prettyplease::unparse(&code)).unwrap();
1044    /// ```
1045    ///
1046    /// Then in `lib.rs`:
1047    /// ```ignore
1048    /// include!(concat!(env!("OUT_DIR"), "/embedded.rs"));
1049    /// ```
1050    pub fn generate_embedded(self, crate_name: &str) -> syn::File {
1051        self.generate_embedded_with_options(crate_name, &EmbeddedCodegenOptions::default())
1052    }
1053
1054    /// Like [`Self::generate_embedded`], but with explicit [`EmbeddedCodegenOptions`].
1055    pub fn generate_embedded_with_options(
1056        mut self,
1057        crate_name: &str,
1058        codegen_options: &EmbeddedCodegenOptions,
1059    ) -> syn::File {
1060        let mut env = EmbeddedInstantiateEnv {
1061            boundary: BoundaryGuarantees::collect(&mut self.ir),
1062            ..Default::default()
1063        };
1064        let compiled = self.compile_internal(&mut env);
1065
1066        let root = crate::staging_util::get_this_crate();
1067        let orig_crate_name = quote::format_ident!("{}", crate_name.replace('-', "_"));
1068
1069        let mut items: Vec<syn::Item> = Vec::new();
1070
1071        // Sort location keys for deterministic output.
1072        let mut location_keys: Vec<_> = compiled.all_dfir().keys().collect();
1073        location_keys.sort();
1074
1075        // Build a map from location key to fn_name for lookups.
1076        let fn_names: SparseSecondaryMap<LocationKey, &str> = location_keys
1077            .iter()
1078            .map(|&k| {
1079                let name = self
1080                    .processes
1081                    .get(k)
1082                    .map(|n| n.fn_name.as_str())
1083                    .or_else(|| self.clusters.get(k).map(|n| n.fn_name.as_str()))
1084                    .or_else(|| self.externals.get(k).map(|n| n.fn_name.as_str()))
1085                    .expect("location key not found in any node map");
1086                (k, name)
1087            })
1088            .collect();
1089
1090        for location_key in location_keys {
1091            let graph = compiled.all_dfir()[location_key]
1092                .as_ref()
1093                .unwrap_or_else(|err| {
1094                    panic!(
1095                        "Failed to partition DFIR graph for location {location_key}: {}",
1096                        err.diagnostic
1097                    )
1098                });
1099
1100            // Get the user-provided function name from the node.
1101            let fn_name = fn_names[location_key];
1102            let fn_ident = syn::Ident::new(fn_name, Span::call_site());
1103
1104            // Get inputs for this location, sorted by name.
1105            let mut loc_inputs = env.inputs.get(location_key).cloned().unwrap_or_default();
1106            loc_inputs.sort_by(|a, b| a.ident.cmp(&b.ident));
1107
1108            // Get outputs for this location, sorted by name.
1109            let mut loc_outputs = env.outputs.get(location_key).cloned().unwrap_or_default();
1110            loc_outputs.sort_by(|a, b| a.ident.cmp(&b.ident));
1111
1112            let mut diagnostics = Diagnostics::new();
1113            // `as_code_options` is sparse: it only has entries for locations where a sidecar
1114            // edited the options. Locations without sidecars fall back to the defaults.
1115            let mut as_code_options = compiled
1116                .as_code_options
1117                .get(location_key)
1118                .cloned()
1119                .unwrap_or_default();
1120            // Hydro has already type-checked the dataflow and injects its own type hints into
1121            // the DFIR it emits, so the DFIR type guards are redundant here. Excluding them keeps
1122            // the generated (and user-visible) embedded code compact and reviewable. The
1123            // per-operator wrappers can still be requested for flamegraph attribution.
1124            as_code_options.exclude_type_guards = true;
1125            as_code_options.require_flame_attribution = codegen_options.require_flame_attribution;
1126            // The serialized meta graph + diagnostics JSON blobs are only used for runtime
1127            // introspection / debugging of hand-written DFIR; embedded Hydro programs have no use
1128            // for them, and they are large opaque string literals.
1129            as_code_options.exclude_meta = true;
1130            let dfir_tokens = graph
1131                .as_code_with_options(
1132                    &quote! { __root_dfir_rs },
1133                    &as_code_options,
1134                    quote!(),
1135                    &mut diagnostics,
1136                )
1137                .expect("DFIR inline code generation failed with diagnostics.");
1138
1139            // --- Build module items (cluster info, output struct, network structs) ---
1140            let mut mod_items: Vec<proc_macro2::TokenStream> = Vec::new();
1141            let mut extra_fn_generics: Vec<proc_macro2::TokenStream> = Vec::new();
1142            let mut cluster_params: Vec<proc_macro2::TokenStream> = Vec::new();
1143            let mut output_params: Vec<proc_macro2::TokenStream> = Vec::new();
1144            let mut net_out_params: Vec<proc_macro2::TokenStream> = Vec::new();
1145            let mut net_in_params: Vec<proc_macro2::TokenStream> = Vec::new();
1146            let mut extra_destructure: Vec<proc_macro2::TokenStream> = Vec::new();
1147
1148            // For cluster locations whose code reads their own member ID, add a self_id parameter.
1149            if self.clusters.contains_key(location_key)
1150                && self.clusters_reading_self_id.contains(&location_key)
1151            {
1152                cluster_params.push(quote! {
1153                    __cluster_self_id: #root::location::member_id::TaglessMemberId
1154                });
1155                // Alias to the name the generated DFIR code expects.
1156                let self_id_ident = syn::Ident::new(
1157                    &format!("__hydro_lang_cluster_self_id_{}", location_key),
1158                    Span::call_site(),
1159                );
1160                extra_destructure.push(quote! {
1161                    let #self_id_ident = __cluster_self_id;
1162                });
1163            }
1164
1165            // For any location that needs cluster membership streams, add parameters.
1166            if let Some(loc_memberships) = env.membership_streams.get(location_key) {
1167                let membership_struct_ident =
1168                    syn::Ident::new("EmbeddedMembershipStreams", Span::call_site());
1169
1170                let mem_generic_idents: Vec<syn::Ident> = loc_memberships
1171                    .iter()
1172                    .enumerate()
1173                    .map(|(i, _)| quote::format_ident!("__Mem{}", i))
1174                    .collect();
1175
1176                let mem_field_names: Vec<syn::Ident> = loc_memberships
1177                    .iter()
1178                    .map(|k| {
1179                        let cluster_fn_name = fn_names[*k];
1180                        syn::Ident::new(cluster_fn_name, Span::call_site())
1181                    })
1182                    .collect();
1183
1184                let mem_item = quote! {
1185                    (#root::location::member_id::TaglessMemberId, #root::location::MembershipEvent)
1186                };
1187                // Each member's events, in the order they happened, each once.
1188                let mem_guarantees = EmbeddedStreamGuarantees {
1189                    order: crate::compile::ir::StreamOrder::TotalOrder,
1190                    retry: crate::compile::ir::StreamRetry::ExactlyOnce,
1191                };
1192                let mem_marker_defs = mem_field_names.iter().map(|field| {
1193                    marker_def(
1194                        &root,
1195                        field,
1196                        &format!(
1197                            "The membership stream of the `{field}` cluster.\n\n{}",
1198                            promise_doc(&mem_guarantees, "EmbeddedInputChannel")
1199                        ),
1200                        &mem_guarantees,
1201                    )
1202                });
1203
1204                let struct_fields: Vec<proc_macro2::TokenStream> = mem_field_names
1205                    .iter()
1206                    .zip(mem_generic_idents.iter())
1207                    .map(|(field, generic)| {
1208                        let doc = format!(
1209                            "Each member's `Joined` / `Left` events (marker type: [`membership::{field}`])."
1210                        );
1211                        quote! {
1212                            #[doc = #doc]
1213                            pub #field: #root::compile::embedded_runtime::EmbeddedInputChannel<membership::#field, #generic>
1214                        }
1215                    })
1216                    .collect();
1217
1218                let struct_generics: Vec<proc_macro2::TokenStream> = mem_generic_idents
1219                    .iter()
1220                    .map(|generic| quote! { #generic: __root_dfir_rs::futures::Stream<Item = #mem_item> + Unpin })
1221                    .collect();
1222
1223                for generic in &mem_generic_idents {
1224                    extra_fn_generics.push(
1225                        quote! { #generic: __root_dfir_rs::futures::Stream<Item = #mem_item> + Unpin + 'a },
1226                    );
1227                }
1228
1229                cluster_params.push(quote! {
1230                    __membership: #fn_ident::#membership_struct_ident<#(#mem_generic_idents),*>
1231                });
1232
1233                for (i, field) in mem_field_names.iter().enumerate() {
1234                    let var_ident =
1235                        syn::Ident::new(&format!("__membership_{}", i), Span::call_site());
1236                    extra_destructure.push(quote! {
1237                        let #var_ident = __membership.#field.into_inner();
1238                    });
1239                }
1240
1241                mod_items.push(quote! {
1242                    pub struct #membership_struct_ident<#(#struct_generics),*> {
1243                        #(#struct_fields),*
1244                    }
1245
1246                    /// Marker types for each membership stream, describing each field of
1247                    /// `EmbeddedMembershipStreams`.
1248                    pub mod membership {
1249                        use super::*;
1250                        #(#mem_marker_defs)*
1251                    }
1252                });
1253            }
1254
1255            // Embedded inputs, passed by value in one struct with a named field per input: a
1256            // `Stream` for each stream input and a plain value for each singleton input.
1257            let mut loc_singleton_inputs = env
1258                .singleton_inputs
1259                .get(location_key)
1260                .cloned()
1261                .unwrap_or_default();
1262            loc_singleton_inputs.sort_by(|a, b| a.0.cmp(&b.0));
1263
1264            let mut input_params: Vec<proc_macro2::TokenStream> = Vec::new();
1265            if !loc_inputs.is_empty() || !loc_singleton_inputs.is_empty() {
1266                let input_struct_ident = syn::Ident::new("EmbeddedInputs", Span::call_site());
1267
1268                let input_generic_idents: Vec<syn::Ident> = loc_inputs
1269                    .iter()
1270                    .enumerate()
1271                    .map(|(i, _)| quote::format_ident!("__In{}", i))
1272                    .collect();
1273
1274                // Every stream input must promise what its `embedded_input` declared, which its
1275                // marker's guarantees state.
1276                let input_marker_defs: Vec<proc_macro2::TokenStream> = loc_inputs
1277                    .iter()
1278                    .map(|input| {
1279                        let guarantees = env.boundary.input(location_key, &input.ident);
1280                        marker_def(
1281                            &root,
1282                            &input.ident,
1283                            &format!(
1284                                "The `{}` stream input.\n\n{}",
1285                                input.ident,
1286                                promise_doc(&guarantees, "EmbeddedInputChannel")
1287                            ),
1288                            &guarantees,
1289                        )
1290                    })
1291                    .collect();
1292
1293                let struct_fields: Vec<proc_macro2::TokenStream> = loc_inputs
1294                    .iter()
1295                    .zip(input_generic_idents.iter())
1296                    .map(|(input, generic)| {
1297                        let ident = &input.ident;
1298                        let doc =
1299                            format!("The `{ident}` stream input (marker type: [`inputs::{ident}`]).");
1300                        quote! {
1301                            #[doc = #doc]
1302                            pub #ident: #root::compile::embedded_runtime::EmbeddedInputChannel<inputs::#ident, #generic>
1303                        }
1304                    })
1305                    .chain(
1306                        loc_singleton_inputs
1307                            .iter()
1308                            .map(|(ident, element_type)| quote! { pub #ident: #element_type }),
1309                    )
1310                    .collect();
1311
1312                let struct_generics: Vec<proc_macro2::TokenStream> = loc_inputs
1313                    .iter()
1314                    .zip(input_generic_idents.iter())
1315                    .map(|(input, generic)| {
1316                        let element_type = &input.element_type;
1317                        quote! { #generic: __root_dfir_rs::futures::Stream<Item = #element_type> + Unpin }
1318                    })
1319                    .collect();
1320
1321                for (input, generic) in loc_inputs.iter().zip(input_generic_idents.iter()) {
1322                    let element_type = &input.element_type;
1323                    extra_fn_generics.push(
1324                        quote! { #generic: __root_dfir_rs::futures::Stream<Item = #element_type> + Unpin + 'a },
1325                    );
1326                }
1327
1328                input_params.push(quote! {
1329                    __inputs: #fn_ident::#input_struct_ident<#(#input_generic_idents),*>
1330                });
1331
1332                for input in &loc_inputs {
1333                    let ident = &input.ident;
1334                    extra_destructure.push(quote! { let #ident = __inputs.#ident.into_inner(); });
1335                }
1336                for (ident, _) in &loc_singleton_inputs {
1337                    extra_destructure.push(quote! { let #ident = __inputs.#ident; });
1338                }
1339
1340                let input_markers_mod = (!input_marker_defs.is_empty()).then(|| {
1341                    quote! {
1342                        /// Marker types for each stream input, describing each stream field of
1343                        /// `EmbeddedInputs`.
1344                        pub mod inputs {
1345                            use super::*;
1346                            #(#input_marker_defs)*
1347                        }
1348                    }
1349                });
1350                mod_items.push(quote! {
1351                    pub struct #input_struct_ident<#(#struct_generics),*> {
1352                        #(#struct_fields),*
1353                    }
1354
1355                    #input_markers_mod
1356                });
1357            }
1358
1359            // Embedded outputs: one `EmbeddedOutputChannel` sink per output, over a marker whose guarantees
1360            // are what the output stream provides, which the host's acknowledgements must cover.
1361            if !loc_outputs.is_empty() {
1362                let output_struct_ident = syn::Ident::new("EmbeddedOutputs", Span::call_site());
1363
1364                let mut struct_generics: Vec<proc_macro2::TokenStream> = Vec::new();
1365                let mut struct_fields: Vec<proc_macro2::TokenStream> = Vec::new();
1366                let mut generics: Vec<syn::Ident> = Vec::new();
1367                let mut marker_defs: Vec<proc_macro2::TokenStream> = Vec::new();
1368                for (i, output) in loc_outputs.iter().enumerate() {
1369                    let ident = &output.ident;
1370                    let element_type = &output.element_type;
1371                    let generic = quote::format_ident!("__Out{}", i);
1372                    let guarantees = env.boundary.output(location_key, ident);
1373                    marker_defs.push(marker_def(
1374                        &root,
1375                        ident,
1376                        &format!("The `{ident}` output.\n\n{}", acknowledge_doc(&guarantees)),
1377                        &guarantees,
1378                    ));
1379                    let doc = format!("The `{ident}` output (marker type: [`outputs::{ident}`]).");
1380                    let sink_bound = quote! { FnMut(#element_type) };
1381                    struct_generics.push(quote! { #generic: #sink_bound });
1382                    extra_fn_generics.push(quote! { #generic: #sink_bound + 'a });
1383                    struct_fields.push(quote! {
1384                        #[doc = #doc]
1385                        pub #ident: #root::compile::embedded_runtime::EmbeddedOutputChannel<outputs::#ident, #generic>
1386                    });
1387                    generics.push(generic);
1388                    extra_destructure
1389                        .push(quote! { let mut #ident = __outputs.#ident.into_inner(); });
1390                }
1391
1392                output_params.push(quote! {
1393                    __outputs: #fn_ident::#output_struct_ident<#(#generics),*>
1394                });
1395
1396                mod_items.push(quote! {
1397                    pub struct #output_struct_ident<#(#struct_generics),*> {
1398                        #(#struct_fields),*
1399                    }
1400
1401                    /// Marker types for each output, describing each field of `EmbeddedOutputs`.
1402                    pub mod outputs {
1403                        use super::*;
1404                        #(#marker_defs)*
1405                    }
1406                });
1407            }
1408
1409            // Network outputs (sinks), plus a marker type per channel describing it.
1410            if let Some(mut ports) = env.network_outputs.remove(location_key) {
1411                ports.sort_by(|a, b| a.name.cmp(&b.name));
1412                let channels = NetworkPortTokens::new(
1413                    &ports,
1414                    &env.boundary,
1415                    &fn_names,
1416                    "__NetOut",
1417                    true,
1418                    |port| {
1419                        let payload = if let Some(ty) = &port.external_type {
1420                            quote! { #ty }
1421                        } else {
1422                            quote! { #root::runtime_support::dfir_rs::bytes::Bytes }
1423                        };
1424                        if port.is_tagged {
1425                            quote! { (#root::location::member_id::TaglessMemberId, #payload) }
1426                        } else {
1427                            payload
1428                        }
1429                    },
1430                );
1431                let NetworkPortTokens {
1432                    fields,
1433                    field_docs,
1434                    field_types,
1435                    generics,
1436                    items,
1437                    marker_defs,
1438                } = &channels;
1439
1440                for (generic, item) in generics.iter().zip(items) {
1441                    extra_fn_generics.push(quote! { #generic: FnMut(#item) + 'a });
1442                }
1443
1444                net_out_params.push(quote! {
1445                    __network_out: #fn_ident::EmbeddedNetworkOut<#(#generics),*>
1446                });
1447
1448                for (port, field) in ports.iter().zip(fields) {
1449                    let var_ident =
1450                        syn::Ident::new(&format!("__network_out_{}", port.name), Span::call_site());
1451                    extra_destructure
1452                        .push(quote! { let mut #var_ident = __network_out.#field.into_inner(); });
1453                }
1454
1455                mod_items.push(quote! {
1456                    pub struct EmbeddedNetworkOut<#(#generics: FnMut(#items)),*> {
1457                        #(#[doc = #field_docs] pub #fields: #field_types),*
1458                    }
1459
1460                    /// Marker types for the sending end of each of this location's outgoing
1461                    /// channels, describing each field of `EmbeddedNetworkOut`.
1462                    pub mod outgoing {
1463                        use super::*;
1464                        #(#marker_defs)*
1465                    }
1466                });
1467            }
1468
1469            // Network inputs (Stream sources), plus a marker type per channel describing it.
1470            if let Some(mut ports) = env.network_inputs.remove(location_key) {
1471                ports.sort_by(|a, b| a.name.cmp(&b.name));
1472                let channels = NetworkPortTokens::new(
1473                    &ports,
1474                    &env.boundary,
1475                    &fn_names,
1476                    "__NetIn",
1477                    false,
1478                    |port| {
1479                        // With embedded (external) serialization, the raw payload is delivered
1480                        // directly, with no transport `Result` wrapper.
1481                        match (&port.external_type, port.is_tagged) {
1482                            (Some(ty), true) => {
1483                                quote! { (#root::location::member_id::TaglessMemberId, #ty) }
1484                            }
1485                            (Some(ty), false) => quote! { #ty },
1486                            (None, true) => {
1487                                quote! { Result<(#root::location::member_id::TaglessMemberId, __root_dfir_rs::bytes::BytesMut), std::io::Error> }
1488                            }
1489                            (None, false) => {
1490                                quote! { Result<__root_dfir_rs::bytes::BytesMut, std::io::Error> }
1491                            }
1492                        }
1493                    },
1494                );
1495                let NetworkPortTokens {
1496                    fields,
1497                    field_docs,
1498                    field_types,
1499                    generics,
1500                    items,
1501                    marker_defs,
1502                } = &channels;
1503
1504                for (generic, item) in generics.iter().zip(items) {
1505                    extra_fn_generics.push(
1506                        quote! { #generic: __root_dfir_rs::futures::Stream<Item = #item> + Unpin + 'a },
1507                    );
1508                }
1509
1510                net_in_params.push(quote! {
1511                    __network_in: #fn_ident::EmbeddedNetworkIn<#(#generics),*>
1512                });
1513
1514                for (port, field) in ports.iter().zip(fields) {
1515                    let var_ident =
1516                        syn::Ident::new(&format!("__network_in_{}", port.name), Span::call_site());
1517                    extra_destructure
1518                        .push(quote! { let #var_ident = __network_in.#field.into_inner(); });
1519                }
1520
1521                mod_items.push(quote! {
1522                    pub struct EmbeddedNetworkIn<#(#generics: __root_dfir_rs::futures::Stream<Item = #items> + Unpin),*> {
1523                        #(#[doc = #field_docs] pub #fields: #field_types),*
1524                    }
1525
1526                    /// Marker types for the receiving end of each of this location's incoming
1527                    /// channels, describing each field of `EmbeddedNetworkIn`.
1528                    pub mod incoming {
1529                        use super::*;
1530                        #(#marker_defs)*
1531                    }
1532                });
1533            }
1534
1535            // Emit the location's module: its parameter structs and channel marker types.
1536            let output_mod: syn::Item = syn::parse_quote! {
1537                pub mod #fn_ident {
1538                    use super::*;
1539
1540                    #(#mod_items)*
1541                }
1542            };
1543            items.push(output_mod);
1544
1545            // Build the function.
1546            let all_params: Vec<proc_macro2::TokenStream> = cluster_params
1547                .into_iter()
1548                .chain(input_params)
1549                .chain(output_params)
1550                .chain(net_in_params)
1551                .chain(net_out_params)
1552                .collect();
1553
1554            let dfir_metrics_ty = crate::compile::dfir_metrics_state_type(&as_code_options, &root);
1555            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<Metrics = #dfir_metrics_ty> + 'a> };
1556
1557            let func = syn::parse_quote! {
1558                #[allow(unused, non_snake_case, clippy::suspicious_else_formatting)]
1559                pub fn #fn_ident<'a, #(#extra_fn_generics),*>(#(#all_params),*) -> #ret_type {
1560                    #(#extra_destructure)*
1561                    #dfir_tokens
1562                }
1563            };
1564
1565            items.push(func);
1566        }
1567
1568        syn::parse_quote! {
1569            use #orig_crate_name::__staged::__deps::*;
1570            use #root::prelude::*;
1571            use #root::runtime_support::dfir_rs as __root_dfir_rs;
1572            pub use #orig_crate_name::__staged;
1573
1574            #( #items )*
1575        }
1576    }
1577}