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 AsyncFnMut() + 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 deterministic(self, thunk: impl AsyncFnOnce() + RefUnwindSafe) {
135 self.compiled().deterministic(thunk)
136 }
137
138 pub fn compiled(mut self) -> CompiledSim {
140 use dfir_lang::graph::{eliminate_extra_unions_tees, partition_graph};
141
142 let compiled_span = tracing::debug_span!(target: "hydro_build", "sim_compiled").entered();
143 let flow_build_span = tracing::debug_span!(target: "hydro_build", "flow_build").entered();
144
145 let is_multi_version = self.location_version.values().any(|&v| v > 0);
146
147 let mut sim_emit = SimBuilder {
148 process_graphs: BTreeMap::new(),
149 cluster_graphs: BTreeMap::new(),
150 process_tick_dfirs: BTreeMap::new(),
151 cluster_tick_dfirs: BTreeMap::new(),
152 extra_stmts_global: vec![],
153 extra_stmts_cluster: BTreeMap::new(),
154 next_hoff_id: crate::Counter::default(),
155 test_safety_only: self.test_safety_only,
156 skip_consistency_assertions: self.skip_consistency_assertions,
157 channel_tables: BTreeMap::new(),
158 bound_sim_hooks: HashMap::new(),
159 };
160
161 self.externals.insert(
163 LocationKey::FIRST,
164 SimExternal {
165 shared_inner: self.externals_port_registry.clone(),
166 },
167 );
168
169 let mut seen_tees_instantiate: HashMap<_, _> = HashMap::new();
170 let mut seen_cluster_members = HashSet::new();
171 self.ir.iter_mut().for_each(|leaf| {
172 leaf.compile_network::<SimDeploy>(
173 &mut SparseSecondaryMap::new(),
174 &mut seen_tees_instantiate,
175 &mut seen_cluster_members,
176 &self.processes,
177 &self.clusters,
178 &self.externals,
179 &mut (),
180 );
181 });
182
183 if is_multi_version {
184 super::versioned_network::splice_versioned_networks(
185 &mut self.ir,
186 &self.location_version_group_root,
187 &self.location_version,
188 );
189 }
190
191 let mut seen_tees = HashMap::new();
192 let mut built_tees = HashMap::new();
193 let mut next_stmt_id = crate::Counter::<StmtId>::default();
194 let mut fold_hooked_idents = HashSet::new();
195 for leaf in &mut self.ir {
196 leaf.emit(
197 &mut sim_emit,
198 &mut seen_tees,
199 &mut built_tees,
200 &mut next_stmt_id,
201 &mut fold_hooked_idents,
202 );
203 }
204
205 fn build_graphs(
206 graphs: BTreeMap<LocationId, FlatGraphBuilder>,
207 ) -> BTreeMap<LocationId, DfirGraph> {
208 graphs
209 .into_iter()
210 .map(|(l, g)| {
211 let FlatGraphBuilderOutput { mut flat_graph, .. } =
212 g.build().expect("Failed to build DFIR flat graph.");
213 eliminate_extra_unions_tees(&mut flat_graph);
214 let partitioned = partition_graph(flat_graph).unwrap_or_else(|err| {
215 panic!(
216 "Failed to partition DFIR graph for location {l:?}: {}",
217 err.diagnostic
218 )
219 });
220 (l, partitioned)
221 })
222 .collect()
223 }
224
225 let process_graphs = build_graphs(sim_emit.process_graphs);
226 let cluster_graphs = build_graphs(sim_emit.cluster_graphs);
227 let process_tick_graphs = build_graphs(sim_emit.process_tick_dfirs);
228 let cluster_tick_graphs = build_graphs(sim_emit.cluster_tick_dfirs);
229
230 #[expect(
231 clippy::disallowed_methods,
232 reason = "nondeterministic iteration order, fine for checks"
233 )]
234 for c in self.clusters.keys() {
235 assert!(
236 self.cluster_max_sizes.contains_key(c),
237 "Cluster {:?} missing max size; call with_cluster_size() before compiled()",
238 c
239 );
240 }
241
242 let (cluster_max_sizes, cluster_member_ids) = self.cluster_sizing();
243 drop(flow_build_span);
244
245 let (bin, trybuild) = create_sim_graph_trybuild(
246 process_graphs,
247 cluster_graphs,
248 cluster_max_sizes,
249 cluster_member_ids,
250 process_tick_graphs,
251 cluster_tick_graphs,
252 sim_emit.extra_stmts_global,
253 sim_emit.extra_stmts_cluster,
254 );
255
256 let out = compile_sim(bin, trybuild).unwrap();
257 let lib = {
258 let _span = tracing::debug_span!(target: "hydro_build", "load_dylib").entered();
259 unsafe { Library::new(&*out).unwrap() }
260 };
261 drop(compiled_span);
262
263 CompiledSim {
264 _path: out,
265 lib,
266 externals_port_registry: self.externals_port_registry.take(),
267 unit_test_fuzz_iterations: self.unit_test_fuzz_iterations,
268 }
269 }
270
271 fn cluster_sizing(
278 &self,
279 ) -> (
280 SparseSecondaryMap<LocationKey, usize>,
281 BTreeMap<LocationId, Vec<u32>>,
282 ) {
283 let mut sizes_by_group_root: BTreeMap<LocationKey, BTreeMap<u32, usize>> = BTreeMap::new();
285 #[expect(
286 clippy::disallowed_methods,
287 reason = "each cluster key is unique; iteration order does not affect the result"
288 )]
289 for key in self.clusters.keys() {
290 let group_root = self.location_version_group_root[key];
291 let version = self.location_version[key];
292 let size = *self.cluster_max_sizes.get(key).unwrap_or_else(|| {
293 panic!(
294 "cluster {key:?} missing max size; `compiled()` asserts every cluster has one \
295 before calling `cluster_sizing`"
296 )
297 });
298 let prev = sizes_by_group_root
299 .entry(group_root)
300 .or_default()
301 .insert(version, size);
302 assert!(
303 prev.is_none(),
304 "multi-version simulation has two corresponding clusters at the same version; \
305 each `next_version()` call must advance to a distinct version"
306 );
307 }
308
309 let mut cluster_sizes: SparseSecondaryMap<LocationKey, usize> = SparseSecondaryMap::new();
312 let mut cluster_member_ids: BTreeMap<LocationId, Vec<u32>> = BTreeMap::new();
313 #[expect(
314 clippy::disallowed_methods,
315 reason = "each cluster key is unique; iteration order does not affect the result"
316 )]
317 for key in self.clusters.keys() {
318 let group_root = self.location_version_group_root[key];
319 let version = self.location_version[key];
320 let per_version = &sizes_by_group_root[&group_root];
321 let merged_total: usize = per_version.values().sum();
322 let offset: u32 = per_version.range(..version).map(|(_, &n)| n as u32).sum();
323 let size = *per_version
324 .get(&version)
325 .expect("every (group, version) was recorded by the first pass above")
326 as u32;
327 cluster_sizes.insert(key, merged_total);
328 cluster_member_ids.insert(LocationId::Cluster(key), (offset..offset + size).collect());
329 }
330
331 (cluster_sizes, cluster_member_ids)
332 }
333}