1use std::collections::{HashMap, HashSet};
8use std::path::{Path, PathBuf};
9use std::sync::atomic::{AtomicUsize, Ordering};
10use std::sync::{Arc, Mutex, OnceLock};
11
12use tracing::{debug, debug_span, info, info_span, warn};
13
14use crate::error::LoadError;
15use crate::ir;
16use crate::lower;
17use crate::mib::Mib;
18use crate::parser;
19use crate::scan;
20use crate::searchpath;
21use crate::source::Source;
22use crate::types::{DiagnosticConfig, ResolverStrictness};
23
24pub struct Loader {
59 sources: Vec<Box<dyn Source>>,
60 modules: Option<Vec<String>>,
61 resolver_strictness: ResolverStrictness,
62 diag_config: DiagnosticConfig,
63 system_paths: bool,
64 parallelism: Option<usize>,
65}
66
67impl Default for Loader {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl Loader {
74 pub fn new() -> Self {
78 Loader {
79 sources: Vec::new(),
80 modules: None,
81 resolver_strictness: ResolverStrictness::Normal,
82 diag_config: DiagnosticConfig::default(),
83 system_paths: false,
84 parallelism: None,
85 }
86 }
87
88 pub fn source(mut self, src: Box<dyn Source>) -> Self {
93 self.sources.push(src);
94 self
95 }
96
97 pub fn sources(mut self, srcs: Vec<Box<dyn Source>>) -> Self {
101 self.sources.extend(srcs);
102 self
103 }
104
105 pub fn modules(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
109 let names: Vec<String> = names.into_iter().map(|n| n.into()).collect();
110 self.modules = Some(names);
111 self
112 }
113
114 pub fn diagnostic_config(mut self, config: DiagnosticConfig) -> Self {
117 self.diag_config = config;
118 self
119 }
120
121 pub fn resolver_strictness(mut self, strictness: ResolverStrictness) -> Self {
123 self.resolver_strictness = strictness;
124 self
125 }
126
127 pub fn system_paths(mut self) -> Self {
133 self.system_paths = true;
134 self
135 }
136
137 pub fn parallelism(mut self, threads: usize) -> Self {
143 self.parallelism = Some(threads.max(1));
144 self
145 }
146}
147
148pub fn load(options: Loader) -> Result<Mib, LoadError> {
161 let requested_module_count = options.modules.as_ref().map_or(0, Vec::len);
162 let load_mode = if options.modules.is_some() {
163 "modules"
164 } else {
165 "all"
166 };
167 let span = info_span!(
168 target: "mib_rs::load",
169 "load",
170 component = "load",
171 mode = load_mode,
172 explicit_source_count = options.sources.len(),
173 requested_module_count = requested_module_count,
174 system_paths = options.system_paths,
175 strictness = ?options.resolver_strictness,
176 reporting = ?options.diag_config.reporting,
177 );
178 let _guard = span.enter();
179
180 let mut sources = options.sources;
181
182 if options.system_paths {
183 debug!(
184 target: "mib_rs::load",
185 component = "load",
186 phase = "source_discovery",
187 "discovering system sources",
188 );
189 sources.extend(searchpath::discover_system_sources());
190 }
191 if sources.is_empty() {
192 return Err(LoadError::NoSources);
193 }
194
195 let strictness = options.resolver_strictness;
196 let diag_config = options.diag_config;
197
198 let (ir_modules, requested_names) = if let Some(names) = options.modules {
199 let mods = load_modules_by_name(&sources, &names, &diag_config)?;
200 (mods, Some(names))
201 } else {
202 let mods = load_all_modules(&sources, &diag_config, options.parallelism)?;
203 (mods, None)
204 };
205
206 debug!(
207 target: "mib_rs::load",
208 component = "load",
209 module_count = ir_modules.len(),
210 phase = "resolve",
211 "load pipeline complete, starting resolver",
212 );
213 let mib = crate::mib::resolver::resolve(ir_modules, strictness, &diag_config);
214
215 check_load_result(&mib, &diag_config, requested_names.as_deref())?;
216
217 info!(
218 target: "mib_rs::load",
219 component = "load",
220 module_count = mib.modules_slice().len(),
221 type_count = mib.types_slice().len(),
222 node_count = mib.tree().len(),
223 diagnostic_count = mib.diagnostics().len(),
224 "load complete",
225 );
226 Ok(mib)
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use std::sync::atomic::{AtomicUsize, Ordering};
233 use std::sync::{Barrier, Condvar, mpsc};
234 use std::time::Duration;
235
236 fn cache_key(index: usize) -> FileCacheKey {
237 (0, format!("TEST-{index}"), index, PathBuf::from("<test>"))
238 }
239
240 #[test]
241 fn cache_initializes_distinct_entries_concurrently() {
242 let cache = Arc::new(Mutex::new(HashMap::new()));
243 let gate = Arc::new((Mutex::new(false), Condvar::new()));
244 let (started_tx, started_rx) = mpsc::channel();
245 let mut handles = Vec::new();
246
247 for index in 0..2 {
248 let cache = Arc::clone(&cache);
249 let gate = Arc::clone(&gate);
250 let started_tx = started_tx.clone();
251 handles.push(std::thread::spawn(move || {
252 cached_modules(&cache, cache_key(index), || {
253 started_tx.send(index).unwrap();
254 let (released, wake) = &*gate;
255 let mut released = released.lock().unwrap();
256 while !*released {
257 released = wake.wait(released).unwrap();
258 }
259 Vec::new()
260 })
261 }));
262 }
263 drop(started_tx);
264
265 let first_started = started_rx.recv_timeout(Duration::from_secs(5));
266 let second_started = started_rx.recv_timeout(Duration::from_secs(5));
267
268 let (released, wake) = &*gate;
269 *released.lock().unwrap() = true;
270 wake.notify_all();
271 for handle in handles {
272 handle.join().unwrap();
273 }
274
275 assert!(first_started.is_ok(), "no cache initializer started");
276 assert!(
277 second_started.is_ok(),
278 "distinct cache entry was blocked by another entry's initializer"
279 );
280 }
281
282 #[test]
283 fn cache_initializes_shared_entry_once() {
284 let cache = Arc::new(Mutex::new(HashMap::new()));
285 let start = Arc::new(Barrier::new(3));
286 let initialization_count = Arc::new(AtomicUsize::new(0));
287 let mut handles = Vec::new();
288
289 for _ in 0..2 {
290 let cache = Arc::clone(&cache);
291 let start = Arc::clone(&start);
292 let initialization_count = Arc::clone(&initialization_count);
293 handles.push(std::thread::spawn(move || {
294 start.wait();
295 cached_modules(&cache, cache_key(0), || {
296 initialization_count.fetch_add(1, Ordering::Relaxed);
297 Vec::new()
298 })
299 }));
300 }
301 start.wait();
302
303 let first = handles.remove(0).join().unwrap();
304 let second = handles.remove(0).join().unwrap();
305 assert_eq!(initialization_count.load(Ordering::Relaxed), 1);
306 assert!(Arc::ptr_eq(&first, &second));
307 }
308}
309
310impl Loader {
311 pub fn load(self) -> Result<Mib, LoadError> {
323 load(self)
324 }
325}
326
327type FileCacheKey = (usize, String, usize, PathBuf);
328type ModuleCacheEntry = Arc<OnceLock<Arc<Vec<ir::Module>>>>;
329type SharedModuleCache = Mutex<HashMap<FileCacheKey, ModuleCacheEntry>>;
330
331fn cached_modules(
332 cache: &SharedModuleCache,
333 key: FileCacheKey,
334 decode: impl FnOnce() -> Vec<ir::Module>,
335) -> Arc<Vec<ir::Module>> {
336 let entry = {
337 let mut cache = cache.lock().unwrap();
338 cache
339 .entry(key)
340 .or_insert_with(|| Arc::new(OnceLock::new()))
341 .clone()
342 };
343
344 entry.get_or_init(|| Arc::new(decode())).clone()
345}
346
347#[derive(Debug)]
348struct ModuleCandidate {
349 source_index: usize,
350 name: String,
351}
352
353fn load_all_modules(
355 sources: &[Box<dyn Source>],
356 diag_config: &DiagnosticConfig,
357 parallelism: Option<usize>,
358) -> Result<Vec<ir::Module>, LoadError> {
359 let mut module_indexes = HashMap::<String, usize>::new();
362 let mut all_modules: Vec<(String, Vec<ModuleCandidate>)> = Vec::new();
363 for (source_index, source) in sources.iter().enumerate() {
364 let names = source.list_modules().map_err(LoadError::Io)?;
365 let mut seen_in_source = HashSet::new();
366 for name in names {
367 if !seen_in_source.insert(name.clone()) {
368 continue;
369 }
370 let candidate = ModuleCandidate {
371 source_index,
372 name: name.clone(),
373 };
374 if let Some(&index) = module_indexes.get(&name) {
375 all_modules[index].1.push(candidate);
376 } else {
377 module_indexes.insert(name.clone(), all_modules.len());
378 all_modules.push((name, vec![candidate]));
379 }
380 }
381 }
382
383 if all_modules.is_empty() {
384 let base = collect_base_modules(HashMap::new());
385 return Ok(base);
386 }
387
388 info!(
389 target: "mib_rs::load",
390 component = "load",
391 phase = "parallel_decode",
392 module_count = all_modules.len(),
393 "parallel loading",
394 );
395
396 let path_cache: SharedModuleCache = Mutex::new(HashMap::new());
399
400 let thread_count =
402 parallelism.unwrap_or_else(|| std::thread::available_parallelism().map_or(1, |n| n.get()));
403 let next_idx = AtomicUsize::new(0);
404 let error: Mutex<Option<LoadError>> = Mutex::new(None);
405 let collected: Mutex<HashMap<String, ir::Module>> = Mutex::new(HashMap::new());
406
407 std::thread::scope(|s| {
408 for _ in 0..thread_count {
409 s.spawn(|| {
410 let mut local_modules: Vec<(String, ir::Module)> = Vec::new();
411 loop {
412 let idx = next_idx.fetch_add(1, Ordering::Relaxed);
413 if idx >= all_modules.len() {
414 break;
415 }
416 if error.lock().unwrap().is_some() {
418 break;
419 }
420
421 let (name, candidates) = &all_modules[idx];
422 let span = debug_span!(
423 target: "mib_rs::load",
424 "load_module",
425 component = "load",
426 module = %name,
427 );
428 let _guard = span.enter();
429
430 'sources: for candidate in candidates {
431 let src = &sources[candidate.source_index];
432 for (candidate_index, result) in
433 src.find_candidates(&candidate.name).enumerate()
434 {
435 let result = match result {
436 Ok(result) => result,
437 Err(error_value) => {
438 *error.lock().unwrap() = Some(LoadError::Io(error_value));
439 break 'sources;
440 }
441 };
442 let cached = cached_modules(
443 &path_cache,
444 (
445 candidate.source_index,
446 name.clone(),
447 candidate_index,
448 result.path.clone(),
449 ),
450 || decode_modules(&result.content, &result.path, diag_config),
451 );
452
453 if let Some(target) = cached.iter().find(|m| m.name == *name) {
456 local_modules.push((name.clone(), target.clone()));
457 break 'sources;
458 }
459 debug!(
460 target: "mib_rs::load",
461 component = "load",
462 module = %name,
463 source_index = candidate.source_index,
464 path = %result.path.display(),
465 reason = "decoded_module_missing",
466 "candidate did not contain advertised module",
467 );
468 }
469 }
470 }
471 let mut map = collected.lock().unwrap();
473 for (name, module) in local_modules {
474 map.entry(name).or_insert(module);
475 }
476 });
477 }
478 });
479
480 if let Some(e) = error.into_inner().unwrap() {
481 return Err(e);
482 }
483 let modules = collected.into_inner().unwrap();
484
485 info!(
486 target: "mib_rs::load",
487 component = "load",
488 phase = "parallel_decode",
489 module_count = modules.len(),
490 "parallel loading complete",
491 );
492
493 Ok(collect_base_modules(modules))
494}
495
496fn load_modules_by_name(
498 sources: &[Box<dyn Source>],
499 names: &[String],
500 diag_config: &DiagnosticConfig,
501) -> Result<Vec<ir::Module>, LoadError> {
502 let mut modules: HashMap<String, ir::Module> = HashMap::new();
503 let mut file_cache: HashMap<FileCacheKey, Vec<ir::Module>> = HashMap::new();
504
505 fn load_one(
506 name: &str,
507 sources: &[Box<dyn Source>],
508 modules: &mut HashMap<String, ir::Module>,
509 file_cache: &mut HashMap<FileCacheKey, Vec<ir::Module>>,
510 diag_config: &DiagnosticConfig,
511 ) -> Result<(), LoadError> {
512 if modules.contains_key(name) {
513 return Ok(());
514 }
515
516 if let Some(base) = lower::base_modules::get_base_module(name) {
518 modules.insert(name.to_string(), base.clone());
519 return Ok(());
520 }
521
522 let mut target = None;
526 'sources: for (source_index, source) in sources.iter().enumerate() {
527 for (candidate_index, result) in source.find_candidates(name).enumerate() {
528 let result = result.map_err(LoadError::Io)?;
529 let mods = file_cache
530 .entry((
531 source_index,
532 name.to_string(),
533 candidate_index,
534 result.path.clone(),
535 ))
536 .or_insert_with(|| decode_modules(&result.content, &result.path, diag_config));
537 if let Some(module) = mods.iter().find(|module| module.name == name) {
538 target = Some(module.clone());
539 break 'sources;
540 }
541
542 debug!(
543 target: "mib_rs::load",
544 component = "load",
545 module = %name,
546 path = %result.path.display(),
547 reason = "decoded_module_missing",
548 "candidate did not contain advertised module",
549 );
550 }
551 }
552
553 let target = match target {
554 Some(target) => target,
555 None => {
556 debug!(
557 target: "mib_rs::load",
558 component = "load",
559 module = %name,
560 reason = "not_found",
561 "module not found",
562 );
563 return Ok(());
564 }
565 };
566
567 let import_modules: Vec<String> = target
569 .imports
570 .iter()
571 .map(|imp| imp.module.clone())
572 .collect::<HashSet<_>>()
573 .into_iter()
574 .collect();
575
576 modules.insert(name.to_string(), target);
577
578 for dep in import_modules {
580 load_one(&dep, sources, modules, file_cache, diag_config)?;
581 }
582
583 Ok(())
584 }
585
586 for name in names {
587 load_one(name, sources, &mut modules, &mut file_cache, diag_config)?;
588 }
589
590 Ok(collect_base_modules(modules))
591}
592
593fn collect_base_modules(mut modules: HashMap<String, ir::Module>) -> Vec<ir::Module> {
595 for &name in lower::base_modules::base_module_names() {
596 if !modules.contains_key(name)
597 && let Some(base) = lower::base_modules::get_base_module(name)
598 {
599 modules.insert(name.to_string(), base.clone());
600 }
601 }
602 let mut mods: Vec<ir::Module> = modules.into_values().collect();
603 mods.sort_by(|a, b| a.name.cmp(&b.name));
604 mods
605}
606
607fn decode_modules(
609 content: &[u8],
610 source_path: &Path,
611 diag_config: &DiagnosticConfig,
612) -> Vec<ir::Module> {
613 let path_display = source_path.display();
614 let span = debug_span!(
615 target: "mib_rs::load",
616 "decode_modules",
617 component = "load",
618 path = %path_display,
619 byte_count = content.len(),
620 );
621 let _guard = span.enter();
622
623 if !scan::looks_like_mib_content(content) {
624 debug!(
625 target: "mib_rs::load",
626 component = "load",
627 path = %path_display,
628 reason = "heuristic_rejected",
629 "content rejected by heuristic",
630 );
631 return Vec::new();
632 }
633
634 let ast_modules = parser::parse(content, diag_config);
635 let path_str = source_path.to_string_lossy();
636 debug!(
637 target: "mib_rs::load",
638 component = "load",
639 path = %path_display,
640 ast_module_count = ast_modules.len(),
641 "parsed source into AST modules",
642 );
643
644 let mut modules = Vec::new();
645 for am in ast_modules {
646 let mut module = lower::lower(am, content, diag_config);
647 module.source_path = path_str.to_string();
648 modules.push(module);
649 }
650 debug!(
651 target: "mib_rs::load",
652 component = "load",
653 path = %path_display,
654 ir_module_count = modules.len(),
655 "lowered source into IR modules",
656 );
657 modules
658}
659
660fn check_load_result(
662 mib: &Mib,
663 diag_config: &DiagnosticConfig,
664 requested_modules: Option<&[String]>,
665) -> Result<(), LoadError> {
666 if let Some(requested) = requested_modules {
668 let mut missing = Vec::new();
669 for name in requested {
670 if mib.module_by_name(name).is_none() {
671 missing.push(name.clone());
672 }
673 }
674 if !missing.is_empty() {
675 warn!(
676 target: "mib_rs::load",
677 component = "load",
678 reason = "missing_requested_modules",
679 missing_module_count = missing.len(),
680 "requested modules not found",
681 );
682 return Err(LoadError::MissingModules(missing));
683 }
684 }
685
686 for d in mib.diagnostics() {
688 if diag_config.should_fail(d.severity) {
689 warn!(
690 target: "mib_rs::load",
691 component = "load",
692 reason = "diagnostic_threshold",
693 severity = ?d.severity,
694 code = %d.code,
695 "diagnostic threshold exceeded",
696 );
697 return Err(LoadError::DiagnosticThreshold);
698 }
699 }
700
701 Ok(())
702}