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