1use std::collections::{BTreeMap, BTreeSet, VecDeque};
4
5use super::contract::{
6 ClosedWorldBundleBuildErrorV0, ClosedWorldBundleEvidenceV0, ClosedWorldBundleV0,
7 ClosedWorldComposesEdgeV0, ClosedWorldComposesScanStateV0,
8 ClosedWorldInterfaceHashAvailabilityV0, ClosedWorldInterfaceHashEntryV0,
9 ClosedWorldInterfaceHashSetV0, ClosedWorldLinkedModuleV0, ClosedWorldModuleMetadataV0,
10 ClosedWorldModuleReachabilityEvidenceV0, ClosedWorldReachabilityBitsetParityReportV0,
11 ClosedWorldSourcePrecisionSummaryV0, ModuleInstanceKeyV0, ModuleQualifiedSymbolSetV0,
12 ReachabilityIndexV0,
13};
14
15impl ClosedWorldBundleV0 {
16 pub fn try_from_linked_modules(
17 entrypoints: Vec<ModuleInstanceKeyV0>,
18 linked_modules: Vec<ClosedWorldLinkedModuleV0>,
19 ) -> Result<Self, ClosedWorldBundleBuildErrorV0> {
20 Self::try_from_linked_modules_with_metadata(entrypoints, linked_modules, Vec::new())
21 }
22
23 pub fn try_from_linked_modules_with_metadata(
24 entrypoints: Vec<ModuleInstanceKeyV0>,
25 linked_modules: Vec<ClosedWorldLinkedModuleV0>,
26 module_metadata: Vec<ClosedWorldModuleMetadataV0>,
27 ) -> Result<Self, ClosedWorldBundleBuildErrorV0> {
28 if entrypoints.is_empty() {
29 return Err(ClosedWorldBundleBuildErrorV0::EmptyEntrypoints);
30 }
31
32 let mut by_instance = BTreeMap::new();
33 for module in linked_modules {
34 by_instance.insert(module.instance.clone(), module);
35 }
36 let metadata_by_instance = module_metadata
37 .into_iter()
38 .map(|metadata| (metadata.module_instance().clone(), metadata))
39 .collect::<BTreeMap<_, _>>();
40
41 let reachability = compute_reachability(entrypoints.as_slice(), &by_instance)?;
42 let linked_modules = reachability.module_instances().to_vec();
43 let interface_hashes =
44 interface_hashes_for_reachable_modules(&linked_modules, &metadata_by_instance);
45 let source_precision =
46 source_precision_for_reachable_modules(&linked_modules, &metadata_by_instance);
47 let module_reachability_evidence =
48 module_reachability_evidence_for_modules(&by_instance, &metadata_by_instance);
49 let composes_edges = composes_edges_for_workspace_modules(&by_instance);
50 let composes_scan_state =
51 composes_scan_state_for_workspace_modules(&by_instance, &metadata_by_instance);
52 let composes_edge_observation_count = by_instance
53 .values()
54 .map(|module| module.composes_edge_observation_count)
55 .sum();
56 let composes_origin_class_names = by_instance
57 .iter()
58 .map(|(instance, module)| {
59 (
60 instance.clone(),
61 dedupe_symbol_names(module.class_names.as_slice()),
62 )
63 })
64 .collect();
65 let closure_hash = stable_closure_hash(entrypoints.as_slice(), &by_instance, &reachability);
66 #[cfg(feature = "test-support")]
67 crate::record_closed_world_bundle_construction_for_test();
68
69 Ok(Self::seal(
70 entrypoints,
71 linked_modules,
72 reachability,
73 closure_hash,
74 ClosedWorldBundleEvidenceV0 {
75 interface_hashes,
76 source_precision,
77 composes_edges,
78 composes_scan_state,
79 composes_edge_observation_count,
80 composes_origin_class_names,
81 module_reachability_evidence,
82 },
83 ))
84 }
85}
86
87fn composes_scan_state_for_workspace_modules(
88 by_instance: &BTreeMap<ModuleInstanceKeyV0, ClosedWorldLinkedModuleV0>,
89 metadata_by_instance: &BTreeMap<ModuleInstanceKeyV0, ClosedWorldModuleMetadataV0>,
90) -> ClosedWorldComposesScanStateV0 {
91 if by_instance.keys().all(|instance| {
92 metadata_by_instance.get(instance).is_some_and(|metadata| {
93 metadata.composes_scan_state() == ClosedWorldComposesScanStateV0::ScannedClosed
94 })
95 }) {
96 ClosedWorldComposesScanStateV0::ScannedClosed
97 } else {
98 ClosedWorldComposesScanStateV0::SourceSetOpen
99 }
100}
101
102fn composes_edges_for_workspace_modules(
103 by_instance: &BTreeMap<ModuleInstanceKeyV0, ClosedWorldLinkedModuleV0>,
104) -> Vec<ClosedWorldComposesEdgeV0> {
105 let mut edges = by_instance
106 .values()
107 .flat_map(|module| module.composes_edges.iter().cloned())
108 .collect::<Vec<_>>();
109 edges.sort_by(|left, right| {
110 (
111 &left.from_module,
112 &left.from_symbol,
113 &left.to_module,
114 &left.to_symbol,
115 )
116 .cmp(&(
117 &right.from_module,
118 &right.from_symbol,
119 &right.to_module,
120 &right.to_symbol,
121 ))
122 });
123 edges.dedup();
124 edges
125}
126
127fn module_reachability_evidence_for_modules(
128 by_instance: &BTreeMap<ModuleInstanceKeyV0, ClosedWorldLinkedModuleV0>,
129 metadata_by_instance: &BTreeMap<ModuleInstanceKeyV0, ClosedWorldModuleMetadataV0>,
130) -> BTreeMap<ModuleInstanceKeyV0, ClosedWorldModuleReachabilityEvidenceV0> {
131 by_instance
132 .keys()
133 .cloned()
134 .map(|instance| {
135 let evidence = metadata_by_instance
136 .get(&instance)
137 .map(ClosedWorldModuleMetadataV0::reachability_evidence)
138 .unwrap_or_default();
139 (instance, evidence)
140 })
141 .collect()
142}
143
144fn interface_hashes_for_reachable_modules(
145 reachable: &[ModuleInstanceKeyV0],
146 metadata_by_instance: &BTreeMap<ModuleInstanceKeyV0, ClosedWorldModuleMetadataV0>,
147) -> ClosedWorldInterfaceHashSetV0 {
148 ClosedWorldInterfaceHashSetV0::new(
149 reachable
150 .iter()
151 .map(|instance| ClosedWorldInterfaceHashEntryV0 {
152 module_instance: instance.clone(),
153 availability: metadata_by_instance
154 .get(instance)
155 .and_then(|metadata| metadata.interface_hash())
156 .map_or(
157 ClosedWorldInterfaceHashAvailabilityV0::Absent,
158 |interface_hash| ClosedWorldInterfaceHashAvailabilityV0::Known {
159 interface_hash: interface_hash.to_string(),
160 },
161 ),
162 })
163 .collect(),
164 )
165}
166
167fn source_precision_for_reachable_modules(
168 reachable: &[ModuleInstanceKeyV0],
169 metadata_by_instance: &BTreeMap<ModuleInstanceKeyV0, ClosedWorldModuleMetadataV0>,
170) -> Option<ClosedWorldSourcePrecisionSummaryV0> {
171 let mut aggregate = ClosedWorldSourcePrecisionSummaryV0::default();
172 let mut observed = false;
173 for instance in reachable {
174 if let Some(source_precision) = metadata_by_instance
175 .get(instance)
176 .and_then(ClosedWorldModuleMetadataV0::source_precision)
177 {
178 aggregate.merge(source_precision);
179 observed = true;
180 }
181 }
182 observed.then_some(aggregate)
183}
184
185pub fn summarize_closed_world_reachability_bitset_parity_v0(
186 entrypoints: Vec<ModuleInstanceKeyV0>,
187 linked_modules: Vec<ClosedWorldLinkedModuleV0>,
188) -> Result<ClosedWorldReachabilityBitsetParityReportV0, ClosedWorldBundleBuildErrorV0> {
189 if entrypoints.is_empty() {
190 return Err(ClosedWorldBundleBuildErrorV0::EmptyEntrypoints);
191 }
192
193 let mut by_instance = BTreeMap::new();
194 for module in linked_modules {
195 by_instance.insert(module.instance.clone(), module);
196 }
197
198 let btreeset_reachability = compute_reachability(entrypoints.as_slice(), &by_instance)?;
199 let bitset_reachability = compute_reachability_bitset(entrypoints.as_slice(), &by_instance)?;
200 let btreeset_closure_hash =
201 stable_closure_hash(entrypoints.as_slice(), &by_instance, &btreeset_reachability);
202 let bitset_closure_hash =
203 stable_closure_hash(entrypoints.as_slice(), &by_instance, &bitset_reachability);
204 let symbol_name_count = bitset_reachability
205 .class_names()
206 .len()
207 .saturating_add(bitset_reachability.keyframe_names().len())
208 .saturating_add(bitset_reachability.value_names().len())
209 .saturating_add(bitset_reachability.custom_property_names().len());
210
211 Ok(ClosedWorldReachabilityBitsetParityReportV0 {
212 schema_version: "0",
213 product: "omena-parser.closed-world-reachability-bitset-parity",
214 module_instance_count: bitset_reachability.module_instances().len(),
215 symbol_name_count,
216 module_qualified_symbols: btreeset_reachability.module_qualified_symbols().to_vec(),
217 reachability_equal: btreeset_reachability == bitset_reachability,
218 closure_hash_equal: btreeset_closure_hash == bitset_closure_hash,
219 btreeset_closure_hash,
220 bitset_closure_hash,
221 })
222}
223
224fn compute_reachability(
225 entrypoints: &[ModuleInstanceKeyV0],
226 by_instance: &BTreeMap<ModuleInstanceKeyV0, ClosedWorldLinkedModuleV0>,
227) -> Result<ReachabilityIndexV0, ClosedWorldBundleBuildErrorV0> {
228 let mut queue = VecDeque::new();
229 let mut seen = BTreeSet::new();
230 for entrypoint in entrypoints {
231 if !by_instance.contains_key(entrypoint) {
232 return Err(ClosedWorldBundleBuildErrorV0::MissingEntrypoint {
233 module: entrypoint.clone(),
234 });
235 }
236 queue.push_back(entrypoint.clone());
237 }
238
239 while let Some(instance) = queue.pop_front() {
240 if !seen.insert(instance.clone()) {
241 continue;
242 }
243 let Some(module) = by_instance.get(&instance) else {
244 return Err(ClosedWorldBundleBuildErrorV0::MissingEntrypoint { module: instance });
245 };
246 for dependency in &module.dependencies {
247 if !by_instance.contains_key(dependency) {
248 return Err(ClosedWorldBundleBuildErrorV0::MissingDependency {
249 module: instance.clone(),
250 dependency: dependency.clone(),
251 });
252 }
253 queue.push_back(dependency.clone());
254 }
255 }
256
257 reachability_index_from_seen(&seen, by_instance)
258}
259
260fn compute_reachability_bitset(
261 entrypoints: &[ModuleInstanceKeyV0],
262 by_instance: &BTreeMap<ModuleInstanceKeyV0, ClosedWorldLinkedModuleV0>,
263) -> Result<ReachabilityIndexV0, ClosedWorldBundleBuildErrorV0> {
264 for entrypoint in entrypoints {
265 if !by_instance.contains_key(entrypoint) {
266 return Err(ClosedWorldBundleBuildErrorV0::MissingEntrypoint {
267 module: entrypoint.clone(),
268 });
269 }
270 }
271 let dense_index = DenseModuleInstanceIndexV0::from_modules(entrypoints, by_instance);
272 let mut seen = DenseModuleInstanceBitsetV0::new(dense_index.len());
273 let mut queue = entrypoints
274 .iter()
275 .filter_map(|entrypoint| dense_index.index_of(entrypoint))
276 .collect::<VecDeque<_>>();
277
278 while let Some(instance_index) = queue.pop_front() {
279 if !seen.insert(instance_index) {
280 continue;
281 }
282 let instance = dense_index.instance(instance_index);
283 let Some(module) = by_instance.get(instance) else {
284 return Err(ClosedWorldBundleBuildErrorV0::MissingEntrypoint {
285 module: instance.clone(),
286 });
287 };
288 for dependency in &module.dependencies {
289 if !by_instance.contains_key(dependency) {
290 return Err(ClosedWorldBundleBuildErrorV0::MissingDependency {
291 module: instance.clone(),
292 dependency: dependency.clone(),
293 });
294 }
295 if let Some(dependency_index) = dense_index.index_of(dependency) {
296 queue.push_back(dependency_index);
297 }
298 }
299 }
300
301 let seen_instances = dense_index.instances_for_bitset(&seen);
302 reachability_index_from_seen(&seen_instances, by_instance)
303}
304
305fn reachability_index_from_seen(
306 seen: &BTreeSet<ModuleInstanceKeyV0>,
307 by_instance: &BTreeMap<ModuleInstanceKeyV0, ClosedWorldLinkedModuleV0>,
308) -> Result<ReachabilityIndexV0, ClosedWorldBundleBuildErrorV0> {
309 let mut class_names = BTreeSet::new();
310 let mut keyframe_names = BTreeSet::new();
311 let mut value_names = BTreeSet::new();
312 let mut custom_property_names = BTreeSet::new();
313 let mut module_qualified_symbols = Vec::with_capacity(by_instance.len());
314
315 for (instance, module) in by_instance {
316 let reachable = seen.contains(instance);
317 if reachable {
318 class_names.extend(module.class_names.iter().cloned());
319 keyframe_names.extend(module.keyframe_names.iter().cloned());
320 value_names.extend(module.value_names.iter().cloned());
321 custom_property_names.extend(module.custom_property_names.iter().cloned());
322 }
323 let qualified_class_names = if reachable {
324 dedupe_symbol_names(&module.class_names)
325 } else {
326 Vec::new()
327 };
328 let qualified_keyframe_names = if reachable {
329 dedupe_symbol_names(&module.keyframe_names)
330 } else {
331 Vec::new()
332 };
333 let qualified_value_names = if reachable {
334 dedupe_symbol_names(&module.value_names)
335 } else {
336 Vec::new()
337 };
338 let qualified_custom_property_names = if reachable {
339 dedupe_symbol_names(&module.custom_property_names)
340 } else {
341 Vec::new()
342 };
343 module_qualified_symbols.push(ModuleQualifiedSymbolSetV0::new(
344 instance.clone(),
345 reachable,
346 qualified_class_names,
347 qualified_keyframe_names,
348 qualified_value_names,
349 qualified_custom_property_names,
350 ));
351 }
352
353 Ok(ReachabilityIndexV0::from_parts(
354 seen.iter().cloned().collect(),
355 module_qualified_symbols,
356 class_names.into_iter().collect(),
357 keyframe_names.into_iter().collect(),
358 value_names.into_iter().collect(),
359 custom_property_names.into_iter().collect(),
360 ))
361}
362
363fn dedupe_symbol_names(names: &[String]) -> Vec<String> {
364 names
365 .iter()
366 .cloned()
367 .collect::<BTreeSet<_>>()
368 .into_iter()
369 .collect()
370}
371
372#[derive(Debug, Clone)]
373struct DenseModuleInstanceIndexV0 {
374 instances: Vec<ModuleInstanceKeyV0>,
375 positions: BTreeMap<ModuleInstanceKeyV0, usize>,
376}
377
378impl DenseModuleInstanceIndexV0 {
379 fn from_modules(
380 entrypoints: &[ModuleInstanceKeyV0],
381 by_instance: &BTreeMap<ModuleInstanceKeyV0, ClosedWorldLinkedModuleV0>,
382 ) -> Self {
383 let mut instances = BTreeSet::new();
384 instances.extend(entrypoints.iter().cloned());
385 for (instance, module) in by_instance {
386 instances.insert(instance.clone());
387 instances.extend(module.dependencies.iter().cloned());
388 }
389 let instances = instances.into_iter().collect::<Vec<_>>();
390 let positions = instances
391 .iter()
392 .enumerate()
393 .map(|(index, instance)| (instance.clone(), index))
394 .collect::<BTreeMap<_, _>>();
395 Self {
396 instances,
397 positions,
398 }
399 }
400
401 fn len(&self) -> usize {
402 self.instances.len()
403 }
404
405 fn index_of(&self, instance: &ModuleInstanceKeyV0) -> Option<usize> {
406 self.positions.get(instance).copied()
407 }
408
409 fn instance(&self, index: usize) -> &ModuleInstanceKeyV0 {
410 &self.instances[index]
411 }
412
413 fn instances_for_bitset(
414 &self,
415 bitset: &DenseModuleInstanceBitsetV0,
416 ) -> BTreeSet<ModuleInstanceKeyV0> {
417 self.instances
418 .iter()
419 .enumerate()
420 .filter(|(index, _)| bitset.contains(*index))
421 .map(|(_, instance)| instance.clone())
422 .collect()
423 }
424}
425
426#[derive(Debug, Clone)]
427struct DenseModuleInstanceBitsetV0 {
428 words: Vec<u64>,
429}
430
431impl DenseModuleInstanceBitsetV0 {
432 fn new(len: usize) -> Self {
433 Self {
434 words: vec![0; len.div_ceil(64)],
435 }
436 }
437
438 fn insert(&mut self, index: usize) -> bool {
439 let word_index = index / 64;
440 let mask = 1u64 << (index % 64);
441 let word = &mut self.words[word_index];
442 let was_empty = *word & mask == 0;
443 *word |= mask;
444 was_empty
445 }
446
447 fn contains(&self, index: usize) -> bool {
448 self.words
449 .get(index / 64)
450 .is_some_and(|word| word & (1u64 << (index % 64)) != 0)
451 }
452}
453
454fn stable_closure_hash(
455 entrypoints: &[ModuleInstanceKeyV0],
456 by_instance: &BTreeMap<ModuleInstanceKeyV0, ClosedWorldLinkedModuleV0>,
457 reachability: &ReachabilityIndexV0,
458) -> String {
459 let mut hash = StableFnv64::new();
460 hash.piece("omena-parser.closed-world-bundle");
461 for entrypoint in entrypoints {
462 hash.instance(entrypoint);
463 }
464 for instance in reachability.module_instances() {
465 hash.instance(instance);
466 if let Some(module) = by_instance.get(instance) {
467 for dependency in &module.dependencies {
468 hash.instance(dependency);
469 }
470 }
471 }
472 for edge in composes_edges_for_workspace_modules(by_instance) {
473 hash.piece("composes");
474 hash.instance(&edge.from_module);
475 hash.piece(&edge.from_symbol);
476 hash.instance(&edge.to_module);
477 hash.piece(&edge.to_symbol);
478 }
479 for name in reachability.class_names() {
480 hash.piece("class");
481 hash.piece(name);
482 }
483 for name in reachability.keyframe_names() {
484 hash.piece("keyframe");
485 hash.piece(name);
486 }
487 for name in reachability.value_names() {
488 hash.piece("value");
489 hash.piece(name);
490 }
491 for name in reachability.custom_property_names() {
492 hash.piece("custom-property");
493 hash.piece(name);
494 }
495 hash.finish_hex()
496}
497
498struct StableFnv64(u64);
499
500impl StableFnv64 {
501 fn new() -> Self {
502 Self(0xcbf2_9ce4_8422_2325)
503 }
504
505 fn piece(&mut self, value: &str) {
506 for byte in value.as_bytes().iter().copied().chain([0]) {
507 self.0 ^= u64::from(byte);
508 self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
509 }
510 }
511
512 fn instance(&mut self, instance: &ModuleInstanceKeyV0) {
513 self.piece(instance.module().as_str());
514 self.piece(instance.configuration().as_str());
515 }
516
517 fn finish_hex(self) -> String {
518 format!("{:016x}", self.0)
519 }
520}