Skip to main content

omena_parser/closed_world/
authority.rs

1//! Closed-world linking authority: builds sealed bundles from linked-module facts.
2
3use 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}