1use std::any::type_name;
2use std::cell::RefCell;
3use std::collections::HashSet;
4use std::marker::PhantomData;
5use std::rc::{Rc, Weak};
6
7use slotmap::{SecondaryMap, SlotMap};
8
9#[cfg(feature = "build")]
10use super::compiled::CompiledFlow;
11#[cfg(feature = "build")]
12use super::deploy::{DeployFlow, DeployResult};
13#[cfg(feature = "build")]
14use super::deploy_provider::{ClusterSpec, Deploy, ExternalSpec, IntoProcessSpec};
15#[cfg(feature = "build")]
16use super::ir::HydroIrOpMetadata;
17use super::ir::{HydroNode, HydroRoot};
18use crate::location::{Cluster, External, LocationKey, LocationType, Process};
19
20pub enum Sidecar {
23 Simple {
25 location_key: LocationKey,
26 future_expr: Box<syn::Expr>,
27 },
28 Bidi {
32 location_key: LocationKey,
33 sidecar_id: SidecarId,
34 sidecar_closure: Box<syn::Expr>,
35 },
36}
37#[cfg(feature = "sim")]
38#[cfg(stageleft_runtime)]
39use crate::sim::flow::SimFlow;
40use crate::staging_util::Invariant;
41
42#[stageleft::export(ExternalPortId, CycleId, ClockId, SidecarId, StmtId, HandoffId)]
43crate::newtype_counter! {
44 pub struct ExternalPortId(usize);
46
47 pub struct CycleId(usize);
49
50 pub struct ClockId(usize);
52
53 pub struct SidecarId(usize);
55
56 pub struct StmtId(usize);
58
59 pub struct HandoffId(usize);
61}
62
63impl CycleId {
64 #[cfg(feature = "build")]
65 pub(crate) fn as_ident(&self) -> syn::Ident {
66 syn::Ident::new(&format!("cycle_{}", self), proc_macro2::Span::call_site())
67 }
68}
69
70impl SidecarId {
71 pub fn idents(&self) -> (syn::Ident, syn::Ident) {
73 let span = proc_macro2::Span::call_site();
74 (
75 syn::Ident::new(&format!("__hydro_sidecar_{}_stream", self), span),
76 syn::Ident::new(&format!("__hydro_sidecar_{}_sink", self), span),
77 )
78 }
79}
80
81pub(crate) type FlowState = Rc<RefCell<FlowStateInner>>;
82
83pub(crate) struct FlowStateInner {
84 roots: Option<Vec<HydroRoot>>,
88
89 next_external_port: crate::Counter<ExternalPortId>,
91
92 next_cycle_id: crate::Counter<CycleId>,
94
95 next_clock_id: crate::Counter<ClockId>,
97
98 next_sidecar_id: crate::Counter<SidecarId>,
100
101 next_sim_hook_id: usize,
104
105 pub sidecars: Vec<Sidecar>,
108
109 pub(crate) live_collection_nodes: Vec<Weak<RefCell<HydroNode>>>,
115
116 pub(crate) clusters_reading_self_id: HashSet<LocationKey>,
118}
119
120impl FlowStateInner {
121 pub fn next_external_port(&mut self) -> ExternalPortId {
122 self.next_external_port.get_and_increment()
123 }
124
125 pub fn next_cycle_id(&mut self) -> CycleId {
126 self.next_cycle_id.get_and_increment()
127 }
128
129 pub fn next_clock_id(&mut self) -> ClockId {
130 self.next_clock_id.get_and_increment()
131 }
132
133 pub fn next_sidecar_id(&mut self) -> SidecarId {
134 self.next_sidecar_id.get_and_increment()
135 }
136
137 pub fn next_sim_hook_id(&mut self) -> usize {
138 let id = self.next_sim_hook_id;
139 self.next_sim_hook_id += 1;
140 id
141 }
142
143 pub fn push_root(&mut self, root: HydroRoot) {
144 self.roots
145 .as_mut()
146 .expect("Attempted to add a root to a flow that has already been finalized. No roots can be added after the flow has been compiled.")
147 .push(root);
148 }
149
150 pub fn try_push_root(&mut self, root: HydroRoot) {
151 if let Some(roots) = self.roots.as_mut() {
152 roots.push(root);
153 }
154 }
155}
156
157pub struct FlowBuilder<'a> {
158 flow_state: FlowState,
160
161 locations: SlotMap<LocationKey, LocationType>,
163 location_names: SecondaryMap<LocationKey, String>,
165 #[cfg(feature = "sim")]
168 location_version: SecondaryMap<LocationKey, u32>,
169 #[cfg(feature = "sim")]
173 location_version_group_root: SecondaryMap<LocationKey, LocationKey>,
174
175 #[cfg_attr(
177 not(feature = "build"),
178 expect(dead_code, reason = "unused without build")
179 )]
180 flow_name: String,
181
182 finalized: bool,
185
186 _phantom: Invariant<'a>,
191}
192
193impl Drop for FlowBuilder<'_> {
194 fn drop(&mut self) {
195 assert!(
196 self.finalized || std::thread::panicking(),
197 "Dropped FlowBuilder without finalizing, you may have forgotten to call `with_default_optimize`, `optimize_with`, or `finalize`."
198 );
199 }
200}
201
202#[expect(missing_docs, reason = "TODO")]
203impl<'a> FlowBuilder<'a> {
204 #[expect(
206 clippy::new_without_default,
207 reason = "call `new` explicitly, not `default`"
208 )]
209 pub fn new() -> Self {
210 let mut name = std::env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "unknown".to_owned());
211 if let Ok(bin_path) = std::env::current_exe()
212 && let Some(bin_name) = bin_path.file_stem()
213 {
214 name = format!("{}/{}", name, bin_name.display());
215 }
216 Self::with_name(name)
217 }
218
219 pub fn with_name(name: impl Into<String>) -> Self {
221 Self {
222 flow_state: Rc::new(RefCell::new(FlowStateInner {
223 roots: Some(vec![]),
224 next_external_port: crate::Counter::default(),
225 next_cycle_id: crate::Counter::default(),
226 next_clock_id: crate::Counter::default(),
227 next_sidecar_id: crate::Counter::default(),
228 next_sim_hook_id: 0,
229 sidecars: Vec::new(),
230 live_collection_nodes: Vec::new(),
231 clusters_reading_self_id: Default::default(),
232 })),
233 locations: SlotMap::with_key(),
234 location_names: SecondaryMap::new(),
235 #[cfg(feature = "sim")]
236 location_version: SecondaryMap::new(),
237 #[cfg(feature = "sim")]
238 location_version_group_root: SecondaryMap::new(),
239 flow_name: name.into(),
240 finalized: false,
241 _phantom: PhantomData,
242 }
243 }
244
245 pub(crate) fn flow_state(&self) -> &FlowState {
246 &self.flow_state
247 }
248
249 fn insert_location(&mut self, ty: LocationType, name: String) -> LocationKey {
250 let key = self.locations.insert(ty);
251 self.location_names.insert(key, name);
252 #[cfg(feature = "sim")]
253 {
254 self.location_version.insert(key, 0);
255 self.location_version_group_root.insert(key, key);
256 }
257 key
258 }
259
260 pub fn process<P>(&mut self) -> Process<'a, P> {
261 let key = self.insert_location(LocationType::Process, type_name::<P>().to_owned());
262 Process {
263 key,
264 flow_state: self.flow_state().clone(),
265 _phantom: PhantomData,
266 }
267 }
268
269 pub fn cluster<C>(&mut self) -> Cluster<'a, C> {
270 let key = self.insert_location(LocationType::Cluster, type_name::<C>().to_owned());
271 Cluster {
272 key,
273 flow_state: self.flow_state().clone(),
274 _phantom: PhantomData,
275 }
276 }
277
278 pub fn external<E>(&mut self) -> External<'a, E> {
279 let key = self.insert_location(LocationType::External, type_name::<E>().to_owned());
280 External {
281 key,
282 flow_state: self.flow_state().clone(),
283 _phantom: PhantomData,
284 }
285 }
286
287 pub fn sim_hook<B: crate::sim_hooks::SimHook>(&mut self) -> B {
300 let flow_state = self.flow_state.clone();
301 B::create(&mut move || flow_state.borrow_mut().next_sim_hook_id())
302 }
303
304 #[cfg(feature = "sim")]
305 pub fn next_version<C>(&mut self, cluster: &Cluster<'a, C>) -> Cluster<'a, C> {
306 let group_root = self.location_version_group_root[cluster.key];
307 let version = self
308 .location_version_group_root
309 .values()
310 .filter(|&&r| r == group_root)
311 .count() as u32;
312 let key = self.insert_location(LocationType::Cluster, type_name::<C>().to_owned());
313 self.location_version.insert(key, version);
314 self.location_version_group_root.insert(key, group_root);
315 Cluster {
316 key,
317 flow_state: self.flow_state().clone(),
318 _phantom: PhantomData,
319 }
320 }
321}
322
323#[cfg(feature = "build")]
324#[cfg_attr(docsrs, doc(cfg(feature = "build")))]
325#[expect(missing_docs, reason = "TODO")]
326impl<'a> FlowBuilder<'a> {
327 pub fn finalize(mut self) -> super::built::BuiltFlow<'a> {
328 self.finalized = true;
329
330 let mut flow_state = self.flow_state.borrow_mut();
331
332 let live_collection_nodes = std::mem::take(&mut flow_state.live_collection_nodes);
336 for node_cell in live_collection_nodes {
337 if let Some(node_cell) = node_cell.upgrade() {
338 let ir_node = node_cell.replace(HydroNode::Placeholder);
339 if !matches!(ir_node, HydroNode::Placeholder) && !ir_node.is_shared_with_others() {
340 flow_state.push_root(HydroRoot::Null {
341 input: Box::new(ir_node),
342 op_metadata: HydroIrOpMetadata::new(),
343 });
344 }
345 }
346 }
347
348 let mut ir = flow_state.roots.take().unwrap();
349 let sidecars = std::mem::take(&mut flow_state.sidecars);
350 let clusters_reading_self_id = std::mem::take(&mut flow_state.clusters_reading_self_id);
351 drop(flow_state);
352
353 super::ir::unify_atomic_ticks(&mut ir);
354
355 super::built::BuiltFlow {
356 ir,
357 locations: std::mem::take(&mut self.locations),
358 location_names: std::mem::take(&mut self.location_names),
359 sidecars,
360 clusters_reading_self_id,
361 flow_name: std::mem::take(&mut self.flow_name),
362 #[cfg(feature = "sim")]
363 location_version: std::mem::take(&mut self.location_version),
364 #[cfg(feature = "sim")]
365 location_version_group_root: std::mem::take(&mut self.location_version_group_root),
366 _phantom: PhantomData,
367 }
368 }
369
370 pub fn with_default_optimize<D: Deploy<'a>>(self) -> DeployFlow<'a, D> {
371 self.finalize().with_default_optimize()
372 }
373
374 pub fn optimize_with(self, f: impl FnOnce(&mut [HydroRoot])) -> super::built::BuiltFlow<'a> {
375 self.finalize().optimize_with(f)
376 }
377
378 pub fn with_process<P, D: Deploy<'a>>(
379 self,
380 process: &Process<'_, P>,
381 spec: impl IntoProcessSpec<'a, D>,
382 ) -> DeployFlow<'a, D> {
383 self.with_default_optimize().with_process(process, spec)
384 }
385
386 pub fn with_remaining_processes<D: Deploy<'a>, S: IntoProcessSpec<'a, D> + 'a>(
387 self,
388 spec: impl Fn() -> S,
389 ) -> DeployFlow<'a, D> {
390 self.with_default_optimize().with_remaining_processes(spec)
391 }
392
393 pub fn with_external<P, D: Deploy<'a>>(
394 self,
395 process: &External<'_, P>,
396 spec: impl ExternalSpec<'a, D>,
397 ) -> DeployFlow<'a, D> {
398 self.with_default_optimize().with_external(process, spec)
399 }
400
401 pub fn with_remaining_externals<D: Deploy<'a>, S: ExternalSpec<'a, D> + 'a>(
402 self,
403 spec: impl Fn() -> S,
404 ) -> DeployFlow<'a, D> {
405 self.with_default_optimize().with_remaining_externals(spec)
406 }
407
408 pub fn with_cluster<C, D: Deploy<'a>>(
409 self,
410 cluster: &Cluster<'_, C>,
411 spec: impl ClusterSpec<'a, D>,
412 ) -> DeployFlow<'a, D> {
413 self.with_default_optimize().with_cluster(cluster, spec)
414 }
415
416 pub fn with_remaining_clusters<D: Deploy<'a>, S: ClusterSpec<'a, D> + 'a>(
417 self,
418 spec: impl Fn() -> S,
419 ) -> DeployFlow<'a, D> {
420 self.with_default_optimize().with_remaining_clusters(spec)
421 }
422
423 pub fn compile<D: Deploy<'a, InstantiateEnv = ()>>(self) -> CompiledFlow<'a> {
424 self.with_default_optimize::<D>().compile()
425 }
426
427 pub fn deploy<D: Deploy<'a>>(self, env: &mut D::InstantiateEnv) -> DeployResult<'a, D> {
428 self.with_default_optimize().deploy(env)
429 }
430
431 #[cfg(feature = "sim")]
432 pub fn sim(self) -> SimFlow<'a> {
435 self.finalize().sim()
436 }
437
438 pub fn from_built<'b>(built: &super::built::BuiltFlow<'_>) -> FlowBuilder<'b> {
439 FlowBuilder {
440 flow_state: Rc::new(RefCell::new(FlowStateInner {
441 roots: None,
442 next_external_port: crate::Counter::default(),
443 next_cycle_id: crate::Counter::default(),
444 next_clock_id: crate::Counter::default(),
445 next_sidecar_id: crate::Counter::default(),
446 next_sim_hook_id: 0,
447 sidecars: Vec::new(),
448 live_collection_nodes: Vec::new(),
449 clusters_reading_self_id: Default::default(),
450 })),
451 locations: built.locations.clone(),
452 location_names: built.location_names.clone(),
453 #[cfg(feature = "sim")]
454 location_version: built.location_version.clone(),
455 #[cfg(feature = "sim")]
456 location_version_group_root: built.location_version_group_root.clone(),
457 flow_name: built.flow_name.clone(),
458 finalized: false,
459 _phantom: PhantomData,
460 }
461 }
462
463 #[doc(hidden)] pub fn replace_ir(&mut self, roots: Vec<HydroRoot>) {
465 self.flow_state.borrow_mut().roots = Some(roots);
466 }
467
468 #[doc(hidden)] pub fn next_clock_id(&mut self) -> ClockId {
470 self.flow_state.borrow_mut().next_clock_id()
471 }
472
473 #[doc(hidden)] pub fn next_cycle_id(&mut self) -> CycleId {
475 self.flow_state.borrow_mut().next_cycle_id()
476 }
477}