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 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 let partitioned = partition_graph(flat_graph).unwrap_or_else(|err| {
207 panic!(
208 "Failed to partition DFIR graph for location {l:?}: {}",
209 err.diagnostic
210 )
211 });
212 (l, partitioned)
213 })
214 .collect()
215 }
216
217 let process_graphs = build_graphs(sim_emit.process_graphs);
218 let cluster_graphs = build_graphs(sim_emit.cluster_graphs);
219 let process_tick_graphs = build_graphs(sim_emit.process_tick_dfirs);
220 let cluster_tick_graphs = build_graphs(sim_emit.cluster_tick_dfirs);
221
222 #[expect(
223 clippy::disallowed_methods,
224 reason = "nondeterministic iteration order, fine for checks"
225 )]
226 for c in self.clusters.keys() {
227 assert!(
228 self.cluster_max_sizes.contains_key(c),
229 "Cluster {:?} missing max size; call with_cluster_size() before compiled()",
230 c
231 );
232 }
233
234 let (cluster_max_sizes, cluster_member_ids) = self.cluster_sizing();
235 drop(flow_build_span);
236
237 let (bin, trybuild) = create_sim_graph_trybuild(
238 process_graphs,
239 cluster_graphs,
240 cluster_max_sizes,
241 cluster_member_ids,
242 process_tick_graphs,
243 cluster_tick_graphs,
244 sim_emit.extra_stmts_global,
245 sim_emit.extra_stmts_cluster,
246 );
247
248 let out = compile_sim(bin, trybuild).unwrap();
249 let lib = {
250 let _span = tracing::debug_span!(target: "hydro_build", "load_dylib").entered();
251 unsafe { Library::new(&out).unwrap() }
252 };
253 drop(compiled_span);
254
255 CompiledSim {
256 _path: out,
257 lib,
258 externals_port_registry: self.externals_port_registry.take(),
259 unit_test_fuzz_iterations: self.unit_test_fuzz_iterations,
260 }
261 }
262
263 fn cluster_sizing(
270 &self,
271 ) -> (
272 SparseSecondaryMap<LocationKey, usize>,
273 BTreeMap<LocationId, Vec<u32>>,
274 ) {
275 let mut sizes_by_group_root: BTreeMap<LocationKey, BTreeMap<u32, usize>> = BTreeMap::new();
277 #[expect(
278 clippy::disallowed_methods,
279 reason = "each cluster key is unique; iteration order does not affect the result"
280 )]
281 for key in self.clusters.keys() {
282 let group_root = self.location_version_group_root[key];
283 let version = self.location_version[key];
284 let size = *self.cluster_max_sizes.get(key).unwrap_or_else(|| {
285 panic!(
286 "cluster {key:?} missing max size; `compiled()` asserts every cluster has one \
287 before calling `cluster_sizing`"
288 )
289 });
290 let prev = sizes_by_group_root
291 .entry(group_root)
292 .or_default()
293 .insert(version, size);
294 assert!(
295 prev.is_none(),
296 "multi-version simulation has two corresponding clusters at the same version; \
297 each `next_version()` call must advance to a distinct version"
298 );
299 }
300
301 let mut cluster_sizes: SparseSecondaryMap<LocationKey, usize> = SparseSecondaryMap::new();
304 let mut cluster_member_ids: BTreeMap<LocationId, Vec<u32>> = BTreeMap::new();
305 #[expect(
306 clippy::disallowed_methods,
307 reason = "each cluster key is unique; iteration order does not affect the result"
308 )]
309 for key in self.clusters.keys() {
310 let group_root = self.location_version_group_root[key];
311 let version = self.location_version[key];
312 let per_version = &sizes_by_group_root[&group_root];
313 let merged_total: usize = per_version.values().sum();
314 let offset: u32 = per_version.range(..version).map(|(_, &n)| n as u32).sum();
315 let size = *per_version
316 .get(&version)
317 .expect("every (group, version) was recorded by the first pass above")
318 as u32;
319 cluster_sizes.insert(key, merged_total);
320 cluster_member_ids.insert(LocationId::Cluster(key), (offset..offset + size).collect());
321 }
322
323 (cluster_sizes, cluster_member_ids)
324 }
325}