1use std::collections::{HashMap, HashSet};
8use std::sync::atomic::{AtomicUsize, Ordering};
9use std::sync::{Arc, Mutex, OnceLock};
10
11use tracing::{debug, debug_span, info, info_span, warn};
12
13use crate::error::LoadError;
14use crate::ir;
15use crate::lower;
16use crate::mib::Mib;
17use crate::parser;
18use crate::scan;
19use crate::searchpath;
20use crate::source::{CandidateId, Source, SourceCandidate, SourceDocument, SourceSet};
21use crate::types::{DiagnosticConfig, ResolverStrictness};
22
23pub struct Loader {
58 sources: Vec<Box<dyn Source>>,
59 modules: Option<Vec<String>>,
60 resolver_strictness: ResolverStrictness,
61 diag_config: DiagnosticConfig,
62 system_paths: bool,
63 parallelism: Option<usize>,
64}
65
66impl Default for Loader {
67 fn default() -> Self {
68 Self::new()
69 }
70}
71
72impl Loader {
73 pub fn new() -> Self {
77 Loader {
78 sources: Vec::new(),
79 modules: None,
80 resolver_strictness: ResolverStrictness::Normal,
81 diag_config: DiagnosticConfig::default(),
82 system_paths: false,
83 parallelism: None,
84 }
85 }
86
87 pub fn source(mut self, src: Box<dyn Source>) -> Self {
92 self.sources.push(src);
93 self
94 }
95
96 pub fn sources(mut self, srcs: Vec<Box<dyn Source>>) -> Self {
100 self.sources.extend(srcs);
101 self
102 }
103
104 pub fn modules(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
108 let names: Vec<String> = names.into_iter().map(|n| n.into()).collect();
109 self.modules = Some(names);
110 self
111 }
112
113 pub fn diagnostic_config(mut self, config: DiagnosticConfig) -> Self {
116 self.diag_config = config;
117 self
118 }
119
120 pub fn resolver_strictness(mut self, strictness: ResolverStrictness) -> Self {
122 self.resolver_strictness = strictness;
123 self
124 }
125
126 pub fn system_paths(mut self) -> Self {
132 self.system_paths = true;
133 self
134 }
135
136 pub fn parallelism(mut self, threads: usize) -> Self {
142 self.parallelism = Some(threads.max(1));
143 self
144 }
145}
146
147pub fn load(options: Loader) -> Result<Mib, LoadError> {
160 let requested_module_count = options.modules.as_ref().map_or(0, Vec::len);
161 let load_mode = if options.modules.is_some() {
162 "modules"
163 } else {
164 "all"
165 };
166 let span = info_span!(
167 target: "mib_rs::load",
168 "load",
169 component = "load",
170 mode = load_mode,
171 explicit_source_count = options.sources.len(),
172 requested_module_count = requested_module_count,
173 system_paths = options.system_paths,
174 strictness = ?options.resolver_strictness,
175 reporting = ?options.diag_config.reporting,
176 );
177 let _guard = span.enter();
178
179 let has_explicit_sources = !options.sources.is_empty();
180 let has_requested_modules = options.modules.is_some();
181 let mut sources = options.sources;
182
183 if options.system_paths {
184 debug!(
185 target: "mib_rs::load",
186 component = "load",
187 phase = "source_discovery",
188 "discovering system sources",
189 );
190 sources.extend(searchpath::discover_system_sources());
191 }
192 if !has_explicit_sources && !options.system_paths && !has_requested_modules {
193 return Err(LoadError::NoSources);
194 }
195 sources.push(Box::new(crate::source::EmbeddedSource));
196
197 let strictness = options.resolver_strictness;
198 let diag_config = options.diag_config;
199
200 let (loaded, requested_names) = if let Some(names) = options.modules {
201 let loaded = load_modules_by_name(&sources, &names, &diag_config)?;
202 (loaded, Some(names))
203 } else {
204 let loaded = load_all_modules(&sources, &diag_config, options.parallelism)?;
205 (loaded, None)
206 };
207
208 debug!(
209 target: "mib_rs::load",
210 component = "load",
211 module_count = loaded.modules.len(),
212 phase = "resolve",
213 "load pipeline complete, starting resolver",
214 );
215 let mib =
216 crate::mib::resolver::resolve(loaded.modules, loaded.sources, strictness, &diag_config);
217
218 let mib = check_load_result(mib, &diag_config, requested_names.as_deref())?;
219
220 info!(
221 target: "mib_rs::load",
222 component = "load",
223 module_count = mib.modules_slice().len(),
224 type_count = mib.types_slice().len(),
225 node_count = mib.tree().len(),
226 diagnostic_count = mib.diagnostics().len(),
227 "load complete",
228 );
229 Ok(mib)
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::source::{SourceOrigin, SourceRange};
236 use crate::types::{DiagCode, Severity};
237 use std::sync::atomic::{AtomicUsize, Ordering};
238 use std::sync::{Barrier, Condvar, mpsc};
239 use std::time::Duration;
240
241 #[derive(Clone)]
242 struct SharedDocumentSource {
243 names: Vec<String>,
244 candidate: SourceCandidate,
245 }
246
247 impl SharedDocumentSource {
248 fn new(
249 names: impl IntoIterator<Item = impl Into<String>>,
250 identity: &str,
251 origin: SourceOrigin,
252 label: &str,
253 bytes: Arc<[u8]>,
254 ) -> Self {
255 Self {
256 names: names.into_iter().map(Into::into).collect(),
257 candidate: SourceCandidate::new(identity, origin, label, bytes),
258 }
259 }
260 }
261
262 impl Source for SharedDocumentSource {
263 fn find(&self, name: &str) -> std::io::Result<Option<SourceCandidate>> {
264 Ok(self
265 .names
266 .iter()
267 .any(|candidate| candidate == name)
268 .then(|| self.candidate.clone()))
269 }
270
271 fn list_modules(&self) -> std::io::Result<Vec<String>> {
272 Ok(self.names.clone())
273 }
274 }
275
276 fn module_document<'a>(mib: &'a Mib, name: &str) -> &'a SourceDocument {
277 let module = mib
278 .module(name)
279 .unwrap_or_else(|| panic!("missing module {name}"));
280 module
281 .source()
282 .unwrap_or_else(|| panic!("module {name} has no source"))
283 }
284
285 fn assert_retained_range(mib: &Mib, range: SourceRange) {
286 let source = mib
287 .source(range.source())
288 .unwrap_or_else(|| panic!("source {} is not retained", range.source()));
289 source
290 .slice(range)
291 .unwrap_or_else(|error| panic!("invalid retained range {range:?}: {error}"));
292 }
293
294 fn assert_optional_range(mib: &Mib, range: Option<SourceRange>) {
295 if let Some(range) = range {
296 assert_retained_range(mib, range);
297 }
298 }
299
300 fn assert_constraint_ranges(mib: &Mib, ranges: &[crate::mib::Range]) {
301 for range in ranges {
302 assert_optional_range(mib, range.range);
303 }
304 }
305
306 fn assert_named_value_ranges(mib: &Mib, values: &[crate::mib::NamedValue]) {
307 for value in values {
308 assert_retained_range(mib, value.range);
309 }
310 }
311
312 fn assert_syntax_constraint_ranges(mib: &Mib, syntax: &crate::mib::SyntaxConstraints) {
313 assert_constraint_ranges(mib, &syntax.sizes);
314 assert_constraint_ranges(mib, &syntax.declared_sizes);
315 assert_constraint_ranges(mib, &syntax.ranges);
316 assert_constraint_ranges(mib, &syntax.declared_ranges);
317 assert_named_value_ranges(mib, &syntax.enums);
318 assert_named_value_ranges(mib, &syntax.bits);
319 }
320
321 fn assert_entity_ranges(mib: &Mib, entity: &crate::mib::object::EntityData) {
322 assert_optional_range(mib, entity.range);
323 assert_optional_range(mib, entity.status_range);
324 assert_optional_range(mib, entity.description_range);
325 assert_optional_range(mib, entity.reference_range);
326 for oid_ref in &entity.oid_refs {
327 assert_retained_range(mib, oid_ref.range);
328 }
329 }
330
331 fn assert_all_resolved_ranges_are_retained(mib: &Mib) {
332 for module in &mib.modules {
333 if let Some(source_id) = module.source_id {
334 assert!(mib.source(source_id).is_some());
335 }
336 for revision in &module.revisions {
337 assert_retained_range(mib, revision.range);
338 }
339 for import in &module.imports {
340 for symbol in &import.symbols {
341 assert_retained_range(mib, symbol.range);
342 }
343 }
344 }
345
346 assert_optional_range(mib, mib.tree.get(mib.tree.root()).range);
347 for node_id in mib.tree.all_nodes() {
348 assert_optional_range(mib, mib.tree.get(node_id).range);
349 }
350
351 for object in &mib.objects {
352 assert_entity_ranges(mib, &object.entity);
353 for range in [
354 object.syntax_range,
355 object.access_range,
356 object.units_range,
357 object.augments_range,
358 object.default_value_range,
359 ] {
360 assert_optional_range(mib, range);
361 }
362 for index in &object.index {
363 assert_retained_range(mib, index.range);
364 }
365 assert_constraint_ranges(mib, &object.sizes);
366 assert_constraint_ranges(mib, &object.ranges);
367 assert_constraint_ranges(mib, &object.declared_sizes);
368 assert_constraint_ranges(mib, &object.declared_ranges);
369 assert_named_value_ranges(mib, &object.enums);
370 assert_named_value_ranges(mib, &object.bits);
371 }
372
373 for typ in &mib.types {
374 assert_optional_range(mib, typ.range);
375 assert_optional_range(mib, typ.syntax_range);
376 assert_constraint_ranges(mib, &typ.sizes);
377 assert_constraint_ranges(mib, &typ.ranges);
378 assert_constraint_ranges(mib, &typ.effective_sizes);
379 assert_constraint_ranges(mib, &typ.effective_ranges);
380 assert_named_value_ranges(mib, &typ.enums);
381 assert_named_value_ranges(mib, &typ.bits);
382 }
383
384 for notification in &mib.notifications {
385 assert_entity_ranges(mib, ¬ification.entity);
386 }
387 for group in &mib.groups {
388 assert_entity_ranges(mib, &group.entity);
389 }
390 for compliance in &mib.compliances {
391 assert_entity_ranges(mib, &compliance.entity);
392 for module in &compliance.modules {
393 assert_retained_range(mib, module.range);
394 for group in &module.groups {
395 assert_retained_range(mib, group.range);
396 }
397 for object in &module.objects {
398 assert_retained_range(mib, object.range);
399 if let Some(syntax) = &object.syntax {
400 assert_syntax_constraint_ranges(mib, syntax);
401 }
402 if let Some(syntax) = &object.write_syntax {
403 assert_syntax_constraint_ranges(mib, syntax);
404 }
405 }
406 }
407 }
408 for capability in &mib.capabilities {
409 assert_entity_ranges(mib, &capability.entity);
410 for module in &capability.supports {
411 assert_retained_range(mib, module.range);
412 for variation in &module.object_variations {
413 assert_retained_range(mib, variation.range);
414 if let Some(syntax) = &variation.syntax {
415 assert_syntax_constraint_ranges(mib, syntax);
416 }
417 if let Some(syntax) = &variation.write_syntax {
418 assert_syntax_constraint_ranges(mib, syntax);
419 }
420 }
421 for variation in &module.notification_variations {
422 assert_retained_range(mib, variation.range);
423 }
424 }
425 }
426 }
427
428 fn cache_key(index: usize) -> CandidateKey {
429 (0, CandidateId::new(format!("TEST-{index}")))
430 }
431
432 #[test]
433 fn cache_initializes_distinct_entries_concurrently() {
434 let cache = Arc::new(Mutex::new(HashMap::new()));
435 let gate = Arc::new((Mutex::new(false), Condvar::new()));
436 let (started_tx, started_rx) = mpsc::channel();
437 let mut handles = Vec::new();
438
439 for index in 0..2 {
440 let cache = Arc::clone(&cache);
441 let gate = Arc::clone(&gate);
442 let started_tx = started_tx.clone();
443 handles.push(std::thread::spawn(move || {
444 cached_modules(&cache, cache_key(index), || {
445 started_tx.send(index).unwrap();
446 let (released, wake) = &*gate;
447 let mut released = released.lock().unwrap();
448 while !*released {
449 released = wake.wait(released).unwrap();
450 }
451 Vec::new()
452 })
453 }));
454 }
455 drop(started_tx);
456
457 let first_started = started_rx.recv_timeout(Duration::from_secs(5));
458 let second_started = started_rx.recv_timeout(Duration::from_secs(5));
459
460 let (released, wake) = &*gate;
461 *released.lock().unwrap() = true;
462 wake.notify_all();
463 for handle in handles {
464 handle.join().unwrap();
465 }
466
467 assert!(first_started.is_ok(), "no cache initializer started");
468 assert!(
469 second_started.is_ok(),
470 "distinct cache entry was blocked by another entry's initializer"
471 );
472 }
473
474 #[test]
475 fn cache_initializes_shared_entry_once() {
476 let cache = Arc::new(Mutex::new(HashMap::new()));
477 let start = Arc::new(Barrier::new(3));
478 let initialization_count = Arc::new(AtomicUsize::new(0));
479 let mut handles = Vec::new();
480
481 for _ in 0..2 {
482 let cache = Arc::clone(&cache);
483 let start = Arc::clone(&start);
484 let initialization_count = Arc::clone(&initialization_count);
485 handles.push(std::thread::spawn(move || {
486 start.wait();
487 cached_modules(&cache, cache_key(0), || {
488 initialization_count.fetch_add(1, Ordering::Relaxed);
489 Vec::new()
490 })
491 }));
492 }
493 start.wait();
494
495 let first = handles.remove(0).join().unwrap();
496 let second = handles.remove(0).join().unwrap();
497 assert_eq!(initialization_count.load(Ordering::Relaxed), 1);
498 assert!(Arc::ptr_eq(&first, &second));
499 }
500
501 #[test]
502 fn source_registry_interns_provider_candidate_once() {
503 let bytes: Arc<[u8]> = Arc::from(&b"A-MIB DEFINITIONS ::= BEGIN END"[..]);
504 let candidate = SourceCandidate::new(
505 "document-7",
506 crate::source::SourceOrigin::memory("buffer-7"),
507 "untitled MIB",
508 Arc::clone(&bytes),
509 );
510 let mut registry = SourceRegistry::default();
511
512 let (first_key, first) = registry.intern(3, &candidate).unwrap();
513 let (second_key, second) = registry.intern(3, &candidate).unwrap();
514
515 assert_eq!(first_key, second_key);
516 assert!(Arc::ptr_eq(&first, &second));
517 assert_eq!(registry.sources.len(), 1);
518 assert_eq!(first.bytes().as_ptr(), bytes.as_ptr());
519 }
520
521 #[test]
522 fn candidate_identity_is_scoped_by_provider() {
523 let bytes: Arc<[u8]> = Arc::from(&b"contents"[..]);
524 let candidate = SourceCandidate::new(
525 "shared-id",
526 crate::source::SourceOrigin::memory("shared-id"),
527 "shared",
528 bytes,
529 );
530 let mut registry = SourceRegistry::default();
531
532 let (_, first) = registry.intern(0, &candidate).unwrap();
533 let (_, second) = registry.intern(1, &candidate).unwrap();
534
535 assert_ne!(first.id(), second.id());
536 assert_eq!(registry.sources.len(), 2);
537 }
538
539 #[test]
540 fn modules_from_one_document_share_source_id_in_parallel_loading() {
541 let content: Arc<[u8]> = Arc::from(
542 &br#"
543FIRST-MIB DEFINITIONS ::= BEGIN END
544SECOND-MIB DEFINITIONS ::= BEGIN END
545"#[..],
546 );
547 let source = SharedDocumentSource::new(
548 ["FIRST-MIB", "SECOND-MIB"],
549 "both-modules",
550 SourceOrigin::custom("test", "both-modules"),
551 "two modules",
552 content,
553 );
554
555 let mib = Loader::new()
556 .source(Box::new(source))
557 .parallelism(4)
558 .load()
559 .expect("multi-module document should load");
560
561 let first = module_document(&mib, "FIRST-MIB");
562 let second = module_document(&mib, "SECOND-MIB");
563 assert_eq!(first.id(), second.id());
564 assert!(std::ptr::eq(first, second));
565 }
566
567 #[test]
568 fn every_resolved_source_range_is_retained_and_in_bounds() {
569 const CAPABILITIES: &[u8] = br#"
570CAP-RANGE-MIB DEFINITIONS ::= BEGIN
571IMPORTS
572 AGENT-CAPABILITIES FROM SNMPv2-CONF
573 enterprises FROM SNMPv2-SMI;
574
575capRange AGENT-CAPABILITIES
576 PRODUCT-RELEASE "1.0"
577 STATUS current
578 DESCRIPTION "Range invariant capability."
579 SUPPORTS EXAMPLE-FULL-MIB
580 INCLUDES { exScalarGroup }
581 VARIATION exDeviceName
582 ACCESS read-only
583 DESCRIPTION "Read-only."
584 ::= { enterprises 99990 }
585
586END
587"#;
588 let source = crate::source::memory_modules([
589 (
590 "EXAMPLE-FULL-MIB",
591 include_bytes!("../tests/data/example-full-mib.txt").as_slice(),
592 ),
593 ("CAP-RANGE-MIB", CAPABILITIES),
594 ]);
595 let mut diagnostics = DiagnosticConfig::verbose();
596 diagnostics.fail_at = Severity::Fatal;
597 let mib = Loader::new()
598 .source(source)
599 .modules(["EXAMPLE-FULL-MIB", "CAP-RANGE-MIB"])
600 .diagnostic_config(diagnostics)
601 .load()
602 .expect("range invariant fixture should load");
603
604 assert!(!mib.objects.is_empty());
605 assert!(!mib.compliances.is_empty());
606 assert!(!mib.capabilities.is_empty());
607 assert_all_resolved_ranges_are_retained(&mib);
608 }
609
610 #[test]
611 fn distinct_documents_have_distinct_source_ids() {
612 let mib = Loader::new()
613 .source(crate::source::memory_modules([
614 (
615 "FIRST-MIB",
616 b"FIRST-MIB DEFINITIONS ::= BEGIN END".as_slice(),
617 ),
618 (
619 "SECOND-MIB",
620 b"SECOND-MIB DEFINITIONS ::= BEGIN END".as_slice(),
621 ),
622 ]))
623 .modules(["FIRST-MIB", "SECOND-MIB"])
624 .load()
625 .expect("separate memory documents should load");
626
627 assert_ne!(
628 module_document(&mib, "FIRST-MIB").id(),
629 module_document(&mib, "SECOND-MIB").id()
630 );
631 }
632
633 #[test]
634 fn mib_retains_document_and_original_bytes_after_registry_drops() {
635 let bytes: Arc<[u8]> = Arc::from(&b"RETAINED-MIB DEFINITIONS ::= BEGIN END"[..]);
636 let weak_bytes = Arc::downgrade(&bytes);
637 let expected_pointer = bytes.as_ptr();
638 let source = SharedDocumentSource::new(
639 ["RETAINED-MIB"],
640 "retained",
641 SourceOrigin::custom("test", "retained"),
642 "retained source",
643 Arc::clone(&bytes),
644 );
645 drop(bytes);
646
647 let mib = Loader::new()
648 .source(Box::new(source))
649 .modules(["RETAINED-MIB"])
650 .load()
651 .expect("retained source should load");
652
653 let document = module_document(&mib, "RETAINED-MIB");
654 assert_eq!(document.bytes().as_ptr(), expected_pointer);
655 assert_eq!(
656 weak_bytes
657 .upgrade()
658 .expect("Mib should retain source bytes")
659 .as_ptr(),
660 expected_pointer
661 );
662
663 drop(mib);
664 assert!(weak_bytes.upgrade().is_none());
665 }
666
667 #[test]
668 fn resolved_modules_reach_all_source_origin_kinds() {
669 static TEMP_FILE_COUNTER: AtomicUsize = AtomicUsize::new(0);
670
671 let file_path = std::env::temp_dir().join(format!(
672 "mib-rs-source-origin-{}-{}.mib",
673 std::process::id(),
674 TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed)
675 ));
676 std::fs::write(&file_path, b"FILE-ORIGIN-MIB DEFINITIONS ::= BEGIN END")
677 .expect("write temporary MIB");
678 let file_source = crate::source::file(&file_path).expect("create file source");
679
680 let custom = SharedDocumentSource::new(
681 ["CUSTOM-ORIGIN-MIB"],
682 "custom-origin",
683 SourceOrigin::custom("database", "record-7"),
684 "database record 7",
685 Arc::from(&b"CUSTOM-ORIGIN-MIB DEFINITIONS ::= BEGIN END"[..]),
686 );
687 let result = Loader::new()
688 .source(file_source)
689 .source(crate::source::memory(
690 "MEMORY-ORIGIN-MIB",
691 b"MEMORY-ORIGIN-MIB DEFINITIONS ::= BEGIN END".as_slice(),
692 ))
693 .source(Box::new(custom))
694 .modules([
695 "FILE-ORIGIN-MIB",
696 "MEMORY-ORIGIN-MIB",
697 "CUSTOM-ORIGIN-MIB",
698 "SNMPv2-SMI",
699 ])
700 .load();
701 std::fs::remove_file(&file_path).expect("remove temporary MIB");
702 let mib = result.expect("all source origin kinds should load");
703
704 assert!(matches!(
705 module_document(&mib, "FILE-ORIGIN-MIB").origin(),
706 SourceOrigin::File { path } if path == &file_path
707 ));
708 assert_eq!(
709 module_document(&mib, "MEMORY-ORIGIN-MIB").origin(),
710 &SourceOrigin::memory("MEMORY-ORIGIN-MIB")
711 );
712 assert_eq!(
713 module_document(&mib, "CUSTOM-ORIGIN-MIB").origin(),
714 &SourceOrigin::custom("database", "record-7")
715 );
716 assert_eq!(
717 module_document(&mib, "SNMPv2-SMI").origin(),
718 &SourceOrigin::embedded("SNMPv2-SMI")
719 );
720 }
721
722 #[test]
723 fn generated_records_have_no_source_id() {
724 let module = ir::Module::new("GENERATED-MIB".to_string(), None);
725 assert!(module.source_id.is_none());
726
727 let resolved = crate::mib::module::ModuleData::new("GENERATED-MIB".to_string());
728 assert!(resolved.source_id.is_none());
729
730 let mib = Mib::new();
731 assert!(mib.sources.is_empty());
732 assert!(mib.tree.get(mib.tree.root()).module.is_none());
733
734 let mib = Loader::new()
735 .modules(["SNMPv2-SMI"])
736 .load()
737 .expect("embedded foundation should load");
738 assert_eq!(
739 module_document(&mib, "SNMPv2-SMI").origin(),
740 &SourceOrigin::embedded("SNMPv2-SMI")
741 );
742 let generated_types: Vec<_> = mib.types.iter().filter(|typ| typ.range.is_none()).collect();
743 assert!(!generated_types.is_empty());
744 for typ in generated_types {
745 assert!(typ.range.is_none());
746 assert!(typ.syntax_range.is_none());
747 assert!(typ.sizes.iter().all(|range| range.range.is_none()));
748 assert!(typ.ranges.iter().all(|range| range.range.is_none()));
749 }
750 assert!(mib.root_node().range().is_none());
751 }
752
753 #[test]
754 fn threshold_report_retains_sources_after_failed_mib_is_dropped() {
755 let bytes: Arc<[u8]> = Arc::from(
756 &br#"LEAK-CHECK-MIB { 01 } DEFINITIONS ::= BEGIN
757badName OBJECT IDENTIFIER ::= { iso 99999 }
758END
759"#[..],
760 );
761 let weak_bytes = Arc::downgrade(&bytes);
762 let source = SharedDocumentSource::new(
763 ["LEAK-CHECK-MIB"],
764 "leak-check",
765 SourceOrigin::memory("leak-check"),
766 "leak check",
767 Arc::clone(&bytes),
768 );
769 drop(bytes);
770 let mut diagnostics = DiagnosticConfig::default();
771 diagnostics
772 .overrides
773 .insert(DiagCode::NumberLeadingZero, Severity::Severe);
774
775 let result = Loader::new()
776 .source(Box::new(source))
777 .modules(["LEAK-CHECK-MIB"])
778 .diagnostic_config(diagnostics)
779 .load();
780
781 let Err(LoadError::DiagnosticThreshold { report }) = result else {
782 panic!("expected diagnostic threshold report");
783 };
784 assert!(weak_bytes.upgrade().is_some());
785 let entry = report
786 .iter()
787 .find(|entry| entry.diagnostic().code == DiagCode::NumberLeadingZero)
788 .expect("expected threshold-triggering diagnostic");
789 assert_eq!(entry.slice().unwrap(), Some(&b"01"[..]));
790 assert!(entry.render().unwrap().contains(":1:18-1:20"));
791 drop(report);
792 assert!(weak_bytes.upgrade().is_none());
793 }
794}
795
796impl Loader {
797 pub fn load(self) -> Result<Mib, LoadError> {
810 load(self)
811 }
812}
813
814type CandidateKey = (usize, CandidateId);
815type ModuleCacheEntry = Arc<OnceLock<Arc<Vec<ir::Module>>>>;
816type SharedModuleCache = Mutex<HashMap<CandidateKey, ModuleCacheEntry>>;
817
818#[derive(Debug, Default)]
819struct SourceRegistry {
820 sources: SourceSet,
821 documents: HashMap<CandidateKey, Arc<SourceDocument>>,
822}
823
824impl SourceRegistry {
825 fn intern(
826 &mut self,
827 provider_index: usize,
828 candidate: &SourceCandidate,
829 ) -> Result<(CandidateKey, Arc<SourceDocument>), LoadError> {
830 let key = (provider_index, candidate.identity().clone());
831 if let Some(document) = self.documents.get(&key) {
832 return Ok((key, Arc::clone(document)));
833 }
834
835 let document = self
836 .sources
837 .insert_shared(
838 candidate.origin().clone(),
839 candidate.label(),
840 Arc::clone(candidate.shared_bytes()),
841 )
842 .map_err(LoadError::from_source)?;
843 self.documents.insert(key.clone(), Arc::clone(&document));
844 Ok((key, document))
845 }
846
847 fn into_sources(self) -> SourceSet {
848 self.sources
849 }
850}
851
852struct LoadedModules {
853 modules: Vec<ir::Module>,
854 sources: SourceSet,
855}
856
857fn cached_modules(
858 cache: &SharedModuleCache,
859 key: CandidateKey,
860 decode: impl FnOnce() -> Vec<ir::Module>,
861) -> Arc<Vec<ir::Module>> {
862 let entry = {
863 let mut cache = cache.lock().unwrap();
864 cache
865 .entry(key)
866 .or_insert_with(|| Arc::new(OnceLock::new()))
867 .clone()
868 };
869
870 entry.get_or_init(|| Arc::new(decode())).clone()
871}
872
873#[derive(Debug)]
874struct ModuleCandidate {
875 source_index: usize,
876 name: String,
877}
878
879fn load_all_modules(
881 sources: &[Box<dyn Source>],
882 diag_config: &DiagnosticConfig,
883 parallelism: Option<usize>,
884) -> Result<LoadedModules, LoadError> {
885 let mut module_indexes = HashMap::<String, usize>::new();
888 let mut all_modules: Vec<(String, Vec<ModuleCandidate>)> = Vec::new();
889 for (source_index, source) in sources.iter().enumerate() {
890 let names = source.list_modules().map_err(LoadError::Io)?;
891 let mut seen_in_source = HashSet::new();
892 for name in names {
893 if !seen_in_source.insert(name.clone()) {
894 continue;
895 }
896 let candidate = ModuleCandidate {
897 source_index,
898 name: name.clone(),
899 };
900 if let Some(&index) = module_indexes.get(&name) {
901 all_modules[index].1.push(candidate);
902 } else {
903 module_indexes.insert(name.clone(), all_modules.len());
904 all_modules.push((name, vec![candidate]));
905 }
906 }
907 }
908
909 info!(
910 target: "mib_rs::load",
911 component = "load",
912 phase = "parallel_decode",
913 module_count = all_modules.len(),
914 "parallel loading",
915 );
916
917 let document_cache: SharedModuleCache = Mutex::new(HashMap::new());
920 let source_registry = Mutex::new(SourceRegistry::default());
921
922 let thread_count =
924 parallelism.unwrap_or_else(|| std::thread::available_parallelism().map_or(1, |n| n.get()));
925 let next_idx = AtomicUsize::new(0);
926 let error: Mutex<Option<LoadError>> = Mutex::new(None);
927 let collected: Mutex<HashMap<String, ir::Module>> = Mutex::new(HashMap::new());
928
929 std::thread::scope(|s| {
930 for _ in 0..thread_count {
931 s.spawn(|| {
932 let mut local_modules: Vec<(String, ir::Module)> = Vec::new();
933 loop {
934 let idx = next_idx.fetch_add(1, Ordering::Relaxed);
935 if idx >= all_modules.len() {
936 break;
937 }
938 if error.lock().unwrap().is_some() {
940 break;
941 }
942
943 let (name, candidates) = &all_modules[idx];
944 let span = debug_span!(
945 target: "mib_rs::load",
946 "load_module",
947 component = "load",
948 module = %name,
949 );
950 let _guard = span.enter();
951
952 'sources: for candidate in candidates {
953 let src = &sources[candidate.source_index];
954 for result in src.find_candidates(&candidate.name) {
955 let result = match result {
956 Ok(result) => result,
957 Err(error_value) => {
958 *error.lock().unwrap() = Some(LoadError::Io(error_value));
959 break 'sources;
960 }
961 };
962 let (key, document) = match source_registry
963 .lock()
964 .unwrap()
965 .intern(candidate.source_index, &result)
966 {
967 Ok(interned) => interned,
968 Err(error_value) => {
969 *error.lock().unwrap() = Some(error_value);
970 break 'sources;
971 }
972 };
973 let cached = cached_modules(&document_cache, key, || {
974 decode_modules(&document, diag_config)
975 });
976
977 if let Some(target) = cached.iter().find(|m| m.name == *name) {
980 local_modules.push((name.clone(), target.clone()));
981 break 'sources;
982 }
983 debug!(
984 target: "mib_rs::load",
985 component = "load",
986 module = %name,
987 source_index = candidate.source_index,
988 source = result.label(),
989 reason = "decoded_module_missing",
990 "candidate did not contain advertised module",
991 );
992 }
993 }
994 }
995 let mut map = collected.lock().unwrap();
997 for (name, module) in local_modules {
998 map.entry(name).or_insert(module);
999 }
1000 });
1001 }
1002 });
1003
1004 if let Some(e) = error.into_inner().unwrap() {
1005 return Err(e);
1006 }
1007 let modules = collected.into_inner().unwrap();
1008
1009 info!(
1010 target: "mib_rs::load",
1011 component = "load",
1012 phase = "parallel_decode",
1013 module_count = modules.len(),
1014 "parallel loading complete",
1015 );
1016
1017 let registry = source_registry.into_inner().unwrap();
1018 Ok(LoadedModules {
1019 modules: collect_modules(modules),
1020 sources: registry.into_sources(),
1021 })
1022}
1023
1024fn load_modules_by_name(
1026 sources: &[Box<dyn Source>],
1027 names: &[String],
1028 diag_config: &DiagnosticConfig,
1029) -> Result<LoadedModules, LoadError> {
1030 let mut modules: HashMap<String, ir::Module> = HashMap::new();
1031 let mut document_cache: HashMap<CandidateKey, Vec<ir::Module>> = HashMap::new();
1032 let mut source_registry = SourceRegistry::default();
1033
1034 fn load_one(
1035 name: &str,
1036 sources: &[Box<dyn Source>],
1037 modules: &mut HashMap<String, ir::Module>,
1038 document_cache: &mut HashMap<CandidateKey, Vec<ir::Module>>,
1039 source_registry: &mut SourceRegistry,
1040 diag_config: &DiagnosticConfig,
1041 ) -> Result<(), LoadError> {
1042 if modules.contains_key(name) {
1043 return Ok(());
1044 }
1045
1046 let mut target = None;
1050 'sources: for (source_index, source) in sources.iter().enumerate() {
1051 for result in source.find_candidates(name) {
1052 let result = result.map_err(LoadError::Io)?;
1053 let (key, document) = source_registry.intern(source_index, &result)?;
1054 let mods = document_cache
1055 .entry(key)
1056 .or_insert_with(|| decode_modules(&document, diag_config));
1057 if let Some(module) = mods.iter().find(|module| module.name == name) {
1058 target = Some(module.clone());
1059 break 'sources;
1060 }
1061
1062 debug!(
1063 target: "mib_rs::load",
1064 component = "load",
1065 module = %name,
1066 source = result.label(),
1067 reason = "decoded_module_missing",
1068 "candidate did not contain advertised module",
1069 );
1070 }
1071 }
1072
1073 let target = match target {
1074 Some(target) => target,
1075 None => {
1076 debug!(
1077 target: "mib_rs::load",
1078 component = "load",
1079 module = %name,
1080 reason = "not_found",
1081 "module not found",
1082 );
1083 return Ok(());
1084 }
1085 };
1086
1087 let import_modules: Vec<String> = target
1089 .imports
1090 .iter()
1091 .map(|imp| imp.module.clone())
1092 .collect::<HashSet<_>>()
1093 .into_iter()
1094 .collect();
1095
1096 modules.insert(name.to_string(), target);
1097
1098 for dep in import_modules {
1100 load_one(
1101 &dep,
1102 sources,
1103 modules,
1104 document_cache,
1105 source_registry,
1106 diag_config,
1107 )?;
1108 }
1109
1110 Ok(())
1111 }
1112
1113 for name in names {
1114 load_one(
1115 name,
1116 sources,
1117 &mut modules,
1118 &mut document_cache,
1119 &mut source_registry,
1120 diag_config,
1121 )?;
1122 }
1123
1124 for name in lower::base_modules::base_module_names() {
1127 load_one(
1128 name,
1129 sources,
1130 &mut modules,
1131 &mut document_cache,
1132 &mut source_registry,
1133 diag_config,
1134 )?;
1135 }
1136
1137 Ok(LoadedModules {
1138 modules: collect_modules(modules),
1139 sources: source_registry.into_sources(),
1140 })
1141}
1142
1143fn collect_modules(modules: HashMap<String, ir::Module>) -> Vec<ir::Module> {
1145 let mut mods: Vec<ir::Module> = modules.into_values().collect();
1146 mods.sort_by(|a, b| a.name.cmp(&b.name));
1147 mods
1148}
1149
1150fn decode_modules(document: &SourceDocument, diag_config: &DiagnosticConfig) -> Vec<ir::Module> {
1152 let content = document.bytes();
1153 let source_label = document.label();
1154 let span = debug_span!(
1155 target: "mib_rs::load",
1156 "decode_modules",
1157 component = "load",
1158 source = source_label,
1159 byte_count = content.len(),
1160 );
1161 let _guard = span.enter();
1162
1163 if !scan::looks_like_mib_content(content) {
1164 debug!(
1165 target: "mib_rs::load",
1166 component = "load",
1167 source = source_label,
1168 reason = "heuristic_rejected",
1169 "content rejected by heuristic",
1170 );
1171 return Vec::new();
1172 }
1173
1174 let ast_modules = parser::parse(document, diag_config);
1175 debug!(
1176 target: "mib_rs::load",
1177 component = "load",
1178 source = source_label,
1179 ast_module_count = ast_modules.len(),
1180 "parsed source into AST modules",
1181 );
1182
1183 let mut modules = Vec::new();
1184 for am in ast_modules {
1185 modules.push(lower::lower(am, document, diag_config));
1186 }
1187 debug!(
1188 target: "mib_rs::load",
1189 component = "load",
1190 source = source_label,
1191 ir_module_count = modules.len(),
1192 "lowered source into IR modules",
1193 );
1194 modules
1195}
1196
1197fn check_load_result(
1199 mib: Mib,
1200 diag_config: &DiagnosticConfig,
1201 requested_modules: Option<&[String]>,
1202) -> Result<Mib, LoadError> {
1203 if let Some(requested) = requested_modules {
1205 let mut missing = Vec::new();
1206 for name in requested {
1207 if mib.module_by_name(name).is_none() {
1208 missing.push(name.clone());
1209 }
1210 }
1211 if !missing.is_empty() {
1212 warn!(
1213 target: "mib_rs::load",
1214 component = "load",
1215 reason = "missing_requested_modules",
1216 missing_module_count = missing.len(),
1217 "requested modules not found",
1218 );
1219 return Err(LoadError::MissingModules(missing));
1220 }
1221 }
1222
1223 if let Some(diagnostic) = mib
1225 .diagnostics()
1226 .iter()
1227 .find(|diagnostic| diag_config.should_fail(diagnostic.severity))
1228 {
1229 warn!(
1230 target: "mib_rs::load",
1231 component = "load",
1232 reason = "diagnostic_threshold",
1233 severity = ?diagnostic.severity,
1234 code = %diagnostic.code,
1235 "diagnostic threshold exceeded",
1236 );
1237
1238 let report = mib.into_diagnostic_report();
1239 return Err(LoadError::DiagnosticThreshold { report });
1240 }
1241
1242 Ok(mib)
1243}