1use std::cell::RefCell;
4use std::collections::{BTreeMap, HashMap, HashSet};
5use std::panic::RefUnwindSafe;
6use std::rc::Rc;
7
8use dfir_lang::graph::{DfirGraph, FlatGraphBuilder, FlatGraphBuilderOutput};
9use libloading::Library;
10use slotmap::{SecondaryMap, SparseSecondaryMap};
11
12use super::builder::SimBuilder;
13use super::compiled::{CompiledSim, CompiledSimInstance};
14use super::graph::{SimDeploy, SimExternal, SimNode, compile_sim, create_sim_graph_trybuild};
15use crate::compile::builder::StmtId;
16use crate::compile::ir::HydroRoot;
17use crate::location::LocationKey;
18use crate::location::dynamic::LocationId;
19use crate::prelude::Cluster;
20use crate::sim::graph::SimExternalPortRegistry;
21use crate::staging_util::Invariant;
22
23pub struct SimFlow<'a> {
25 pub(crate) ir: Vec<HydroRoot>,
26
27 pub(crate) processes: SparseSecondaryMap<LocationKey, SimNode>,
29 pub(crate) clusters: SparseSecondaryMap<LocationKey, SimNode>,
31 pub(crate) externals: SparseSecondaryMap<LocationKey, SimExternal>,
33
34 pub(crate) cluster_max_sizes: SparseSecondaryMap<LocationKey, usize>,
36 pub(crate) externals_port_registry: Rc<RefCell<SimExternalPortRegistry>>,
38
39 pub(crate) location_version: SecondaryMap<LocationKey, u32>,
42
43 pub(crate) location_version_group_root: SecondaryMap<LocationKey, LocationKey>,
47
48 pub(crate) test_safety_only: bool,
50
51 pub(crate) skip_consistency_assertions: bool,
55
56 pub(crate) unit_test_fuzz_iterations: usize,
58
59 pub(crate) _phantom: Invariant<'a>,
60}
61
62impl<'a> SimFlow<'a> {
63 pub fn with_cluster_size<C>(mut self, cluster: &Cluster<'a, C>, max_size: usize) -> Self {
65 self.cluster_max_sizes.insert(cluster.key, max_size);
66 self
67 }
68
69 pub fn test_safety_only(mut self) -> Self {
78 self.test_safety_only = true;
79 self
80 }
81
82 pub fn skip_consistency_assertions(mut self) -> Self {
87 self.skip_consistency_assertions = true;
88 self
89 }
90
91 pub fn unit_test_fuzz_iterations(mut self, iterations: usize) -> Self {
94 self.unit_test_fuzz_iterations = iterations;
95 self
96 }
97
98 pub fn with_instance<T>(self, thunk: impl FnOnce(CompiledSimInstance) -> T) -> T {
100 self.compiled().with_instance(thunk)
101 }
102
103 pub fn fuzz(self, thunk: impl AsyncFn() + RefUnwindSafe) {
114 self.compiled().fuzz(thunk)
115 }
116
117 pub fn exhaustive(self, thunk: impl AsyncFnMut() + RefUnwindSafe) -> usize {
128 self.compiled().exhaustive(thunk)
129 }
130
131 pub fn compiled(mut self) -> CompiledSim {
133 use dfir_lang::graph::{eliminate_extra_unions_tees, partition_graph};
134
135 let compiled_span = tracing::debug_span!(target: "hydro_build", "sim_compiled").entered();
136 let flow_build_span = tracing::debug_span!(target: "hydro_build", "flow_build").entered();
137
138 let is_multi_version = self.location_version.values().any(|&v| v > 0);
139
140 let mut sim_emit = SimBuilder {
141 process_graphs: BTreeMap::new(),
142 cluster_graphs: BTreeMap::new(),
143 process_tick_dfirs: BTreeMap::new(),
144 cluster_tick_dfirs: BTreeMap::new(),
145 extra_stmts_global: vec![],
146 extra_stmts_cluster: BTreeMap::new(),
147 next_hoff_id: crate::Counter::default(),
148 test_safety_only: self.test_safety_only,
149 skip_consistency_assertions: self.skip_consistency_assertions,
150 channel_tables: BTreeMap::new(),
151 };
152
153 self.externals.insert(
155 LocationKey::FIRST,
156 SimExternal {
157 shared_inner: self.externals_port_registry.clone(),
158 },
159 );
160
161 let mut seen_tees_instantiate: HashMap<_, _> = HashMap::new();
162 let mut seen_cluster_members = HashSet::new();
163 self.ir.iter_mut().for_each(|leaf| {
164 leaf.compile_network::<SimDeploy>(
165 &mut SparseSecondaryMap::new(),
166 &mut seen_tees_instantiate,
167 &mut seen_cluster_members,
168 &self.processes,
169 &self.clusters,
170 &self.externals,
171 &mut (),
172 );
173 });
174
175 if is_multi_version {
176 super::versioned_network::splice_versioned_networks(
177 &mut self.ir,
178 &self.location_version_group_root,
179 &self.location_version,
180 );
181 }
182
183 let mut seen_tees = HashMap::new();
184 let mut built_tees = HashMap::new();
185 let mut next_stmt_id = crate::Counter::<StmtId>::default();
186 let mut fold_hooked_idents = HashSet::new();
187 for leaf in &mut self.ir {
188 leaf.emit(
189 &mut sim_emit,
190 &mut seen_tees,
191 &mut built_tees,
192 &mut next_stmt_id,
193 &mut fold_hooked_idents,
194 );
195 }
196
197 fn build_graphs(
198 graphs: BTreeMap<LocationId, FlatGraphBuilder>,
199 ) -> BTreeMap<LocationId, DfirGraph> {
200 graphs
201 .into_iter()
202 .map(|(l, g)| {
203 let FlatGraphBuilderOutput { mut flat_graph, .. } =
204 g.build().expect("Failed to build DFIR flat graph.");
205 eliminate_extra_unions_tees(&mut flat_graph);
206 (
207 l,
208 partition_graph(flat_graph).expect("Failed to partition (cycle detected)."),
209 )
210 })
211 .collect()
212 }
213
214 let process_graphs = build_graphs(sim_emit.process_graphs);
215 let cluster_graphs = build_graphs(sim_emit.cluster_graphs);
216 let process_tick_graphs = build_graphs(sim_emit.process_tick_dfirs);
217 let cluster_tick_graphs = build_graphs(sim_emit.cluster_tick_dfirs);
218
219 #[expect(
220 clippy::disallowed_methods,
221 reason = "nondeterministic iteration order, fine for checks"
222 )]
223 for c in self.clusters.keys() {
224 assert!(
225 self.cluster_max_sizes.contains_key(c),
226 "Cluster {:?} missing max size; call with_cluster_size() before compiled()",
227 c
228 );
229 }
230
231 let (cluster_max_sizes, cluster_member_ids) = self.cluster_sizing();
232 drop(flow_build_span);
233
234 let (bin, trybuild) = create_sim_graph_trybuild(
235 process_graphs,
236 cluster_graphs,
237 cluster_max_sizes,
238 cluster_member_ids,
239 process_tick_graphs,
240 cluster_tick_graphs,
241 sim_emit.extra_stmts_global,
242 sim_emit.extra_stmts_cluster,
243 );
244
245 let out = compile_sim(bin, trybuild).unwrap();
246 let lib = {
247 let _span = tracing::debug_span!(target: "hydro_build", "load_dylib").entered();
248 unsafe { Library::new(&out).unwrap() }
249 };
250 drop(compiled_span);
251
252 CompiledSim {
253 _path: out,
254 lib,
255 externals_port_registry: self.externals_port_registry.take(),
256 unit_test_fuzz_iterations: self.unit_test_fuzz_iterations,
257 }
258 }
259
260 fn cluster_sizing(
267 &self,
268 ) -> (
269 SparseSecondaryMap<LocationKey, usize>,
270 BTreeMap<LocationId, Vec<u32>>,
271 ) {
272 let mut sizes_by_group_root: BTreeMap<LocationKey, BTreeMap<u32, usize>> = BTreeMap::new();
274 #[expect(
275 clippy::disallowed_methods,
276 reason = "each cluster key is unique; iteration order does not affect the result"
277 )]
278 for key in self.clusters.keys() {
279 let group_root = self.location_version_group_root[key];
280 let version = self.location_version[key];
281 let size = *self.cluster_max_sizes.get(key).unwrap_or_else(|| {
282 panic!(
283 "cluster {key:?} missing max size; `compiled()` asserts every cluster has one \
284 before calling `cluster_sizing`"
285 )
286 });
287 let prev = sizes_by_group_root
288 .entry(group_root)
289 .or_default()
290 .insert(version, size);
291 assert!(
292 prev.is_none(),
293 "multi-version simulation has two corresponding clusters at the same version; \
294 each `next_version()` call must advance to a distinct version"
295 );
296 }
297
298 let mut cluster_sizes: SparseSecondaryMap<LocationKey, usize> = SparseSecondaryMap::new();
301 let mut cluster_member_ids: BTreeMap<LocationId, Vec<u32>> = BTreeMap::new();
302 #[expect(
303 clippy::disallowed_methods,
304 reason = "each cluster key is unique; iteration order does not affect the result"
305 )]
306 for key in self.clusters.keys() {
307 let group_root = self.location_version_group_root[key];
308 let version = self.location_version[key];
309 let per_version = &sizes_by_group_root[&group_root];
310 let merged_total: usize = per_version.values().sum();
311 let offset: u32 = per_version.range(..version).map(|(_, &n)| n as u32).sum();
312 let size = *per_version
313 .get(&version)
314 .expect("every (group, version) was recorded by the first pass above")
315 as u32;
316 cluster_sizes.insert(key, merged_total);
317 cluster_member_ids.insert(LocationId::Cluster(key), (offset..offset + size).collect());
318 }
319
320 (cluster_sizes, cluster_member_ids)
321 }
322}