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