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