use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::environment::JuliaInstall;
use super::HarvestedLibrary;
use super::harvest::{harvest_entry, harvest_package_named};
use super::model::{DefLocation, ExportedName, ModuleIndex, PackageIndex, Span, Visibility};
const BASE_ENTRIES: &[&str] = &["Base_compiler.jl", "Base.jl"];
const BASE_FALLBACK: &str = include_str!("fallback/base_exports.txt");
const CORE_FALLBACK: &str = include_str!("fallback/core_exports.txt");
pub fn build_system_index(install: Option<&JuliaInstall>) -> BTreeMap<String, Arc<PackageIndex>> {
build_system_library(install).packages
}
pub fn build_system_library(install: Option<&JuliaInstall>) -> HarvestedLibrary {
match install {
Some(install) => harvest_system(install),
None => HarvestedLibrary {
packages: fallback_system(),
roots: BTreeMap::new(),
workspaces: Vec::new(),
},
}
}
fn harvest_system(install: &JuliaInstall) -> HarvestedLibrary {
let mut packages = BTreeMap::new();
let mut roots = BTreeMap::new();
let base = harvest_base(&install.base_dir);
packages.insert("Base".to_string(), Arc::new(base));
roots.insert("Base".to_string(), install.base_dir.clone());
let core = harvest_entry(&install.base_dir, &install.base_dir.join("boot.jl"), "Core");
packages.insert("Core".to_string(), Arc::new(core));
roots.insert("Core".to_string(), install.base_dir.clone());
if let Ok(entries) = std::fs::read_dir(&install.stdlib_dir) {
for entry in entries.filter_map(Result::ok) {
let dir = entry.path();
let Some(name) = dir.file_name().and_then(|n| n.to_str()) else {
continue;
};
if dir.join("src").join(format!("{name}.jl")).is_file() {
let index = harvest_package_named(&dir, name);
packages.insert(name.to_string(), Arc::new(index));
roots.insert(name.to_string(), dir);
}
}
}
HarvestedLibrary {
packages,
roots,
workspaces: Vec::new(),
}
}
fn harvest_base(base_dir: &Path) -> PackageIndex {
let mut merged: Option<PackageIndex> = None;
for entry in BASE_ENTRIES {
let file = base_dir.join(entry);
if !file.is_file() {
continue;
}
let index = harvest_entry(base_dir, &file, "Base");
match &mut merged {
None => merged = Some(index),
Some(acc) => merge_index(acc, index),
}
}
merged.unwrap_or_else(|| harvest_entry(base_dir, &base_dir.join(BASE_ENTRIES[0]), "Base"))
}
fn merge_index(acc: &mut PackageIndex, other: PackageIndex) {
let root = &mut acc.root;
root.exports.extend(other.root.exports);
root.types.extend(other.root.types);
root.consts.extend(other.root.consts);
root.macros.extend(other.root.macros);
root.submodules.extend(other.root.submodules);
for group in other.root.functions {
match root
.functions
.iter_mut()
.find(|g| g.name == group.name && g.owner == group.owner)
{
Some(existing) => existing.methods.extend(group.methods),
None => root.functions.push(group),
}
}
acc.diagnostics.extend(other.diagnostics);
}
fn fallback_system() -> BTreeMap<String, Arc<PackageIndex>> {
let mut out = BTreeMap::new();
out.insert(
"Base".to_string(),
Arc::new(synthetic_index("Base", BASE_FALLBACK)),
);
out.insert(
"Core".to_string(),
Arc::new(synthetic_index("Core", CORE_FALLBACK)),
);
out
}
fn synthetic_index(name: &str, list: &str) -> PackageIndex {
let loc = DefLocation {
file: PathBuf::new(),
range: Span { start: 0, end: 0 },
};
let exports = list
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.map(|export| ExportedName {
name: export.to_string(),
visibility: Visibility::Exported,
loc: loc.clone(),
})
.collect();
PackageIndex {
name: name.to_string(),
root: ModuleIndex {
name: name.to_string(),
bare: name == "Core",
loc,
exports,
functions: Vec::new(),
types: Vec::new(),
consts: Vec::new(),
macros: Vec::new(),
submodules: Vec::new(),
},
members: Vec::new(),
member_modules: Default::default(),
diagnostics: Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fallback_populates_base_and_core_exports() {
let system = build_system_index(None);
let base = system.get("Base").expect("Base index");
let core = system.get("Core").expect("Core index");
let base_names: Vec<&str> = base.root.exports.iter().map(|e| e.name.as_str()).collect();
assert!(
base_names.contains(&"println"),
"Base should export println"
);
assert!(base_names.contains(&"map"), "Base should export map");
assert!(base_names.contains(&"push!"), "Base should export push!");
let core_names: Vec<&str> = core.root.exports.iter().map(|e| e.name.as_str()).collect();
assert!(core_names.contains(&"Int"), "Core should export Int");
assert!(core_names.contains(&"Type"), "Core should export Type");
assert!(core.root.bare, "Core is a baremodule");
assert!(!base.root.bare, "Base is a normal module");
}
#[test]
fn synthetic_index_skips_blanks_and_comments() {
let index = synthetic_index("Base", "# a comment\n\nfoo\n bar \n");
let names: Vec<&str> = index.root.exports.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, vec!["foo", "bar"]);
}
}