1use std::path::{Component, Path, PathBuf};
4
5use harn_stdlib_catalog::{CatalogNotInstalled, StdlibCatalog};
6
7pub fn try_stdlib_catalog() -> Result<&'static StdlibCatalog, CatalogNotInstalled> {
12 ensure_stdlib_installed()?;
13 harn_stdlib_catalog::installed()
14}
15
16pub fn ensure_stdlib_installed() -> Result<(), CatalogNotInstalled> {
18 #[cfg(any(test, feature = "bundled-stdlib"))]
19 let _ = harn_stdlib::install();
20 harn_stdlib_catalog::ensure_installed()
21}
22
23pub fn stdlib_catalog() -> &'static StdlibCatalog {
27 try_stdlib_catalog().unwrap_or_else(|error| panic!("{error}"))
28}
29
30pub(crate) fn get_stdlib_source(module: &str) -> Option<&'static str> {
33 stdlib_catalog().source(module)
34}
35
36pub(crate) fn builtin_reexports(module: &str) -> &'static [&'static str] {
38 harn_stdlib_catalog::builtin_reexports(module)
39}
40
41pub(crate) fn stdlib_virtual_path(module: &str) -> PathBuf {
45 PathBuf::from(format!("<std>/{module}"))
46}
47
48pub(crate) fn is_stdlib_virtual_path(path: &Path) -> bool {
49 path.to_str().is_some_and(|path| path.starts_with("<std>/"))
50}
51
52pub(crate) fn canonical_virtual_path(path: &Path) -> Option<PathBuf> {
60 if is_stdlib_virtual_path(path) {
61 return Some(path.to_path_buf());
62 }
63 is_embedded_stdlib_spelling(path).then(|| crate::manifest_walk::normalize_lexically(path))
64}
65
66pub(crate) fn is_embedded_stdlib_spelling(path: &Path) -> bool {
69 let Some(path) = path.to_str() else {
70 return false;
71 };
72 ["<std>", "<stdlib>"].iter().any(|root| {
73 path.strip_prefix(root)
74 .is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
75 })
76}
77
78pub(crate) fn relative_stdlib_module(current_file: &Path, import_path: &str) -> Option<String> {
84 let current = current_file.to_str()?.strip_prefix("<std>/")?;
85 let mut parts = current.split('/').collect::<Vec<_>>();
86 parts.pop()?;
87
88 for component in Path::new(import_path).components() {
89 match component {
90 Component::CurDir => {}
91 Component::ParentDir => {
92 parts.pop()?;
93 }
94 Component::Normal(part) => parts.push(part.to_str()?),
95 Component::RootDir | Component::Prefix(_) => return None,
96 }
97 }
98
99 let last = parts.last_mut()?;
100 *last = last.strip_suffix(".harn").unwrap_or(last);
101 let module = parts.join("/");
102 get_stdlib_source(&module).map(|_| module)
103}
104
105#[cfg(test)]
106mod tests {
107 #[test]
112 fn compiler_crates_do_not_depend_on_the_embedded_sources() {
113 let crates = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
114 .parent()
115 .expect("crates directory");
116 for (crate_name, allowed_optional) in [("harn-modules", true), ("harn-vm", false)] {
117 let manifest = std::fs::read_to_string(crates.join(crate_name).join("Cargo.toml"))
118 .expect("read manifest");
119 let manifest: toml::Table = toml::from_str(&manifest).expect("parse manifest");
120 let dependency = manifest
121 .get("dependencies")
122 .and_then(|dependencies| dependencies.get("harn-stdlib"));
123 let optional = dependency
124 .and_then(|dependency| dependency.get("optional"))
125 .and_then(toml::Value::as_bool)
126 == Some(true);
127 assert!(
128 dependency.is_none() || (allowed_optional && optional),
129 "{crate_name} must not depend on harn-stdlib; read the stdlib through harn-stdlib-catalog"
130 );
131 }
132 }
133}