Skip to main content

harn_modules/
stdlib.rs

1//! Embedded stdlib source access for the module graph.
2
3use std::path::{Component, Path, PathBuf};
4
5use harn_stdlib_catalog::{CatalogNotInstalled, StdlibCatalog};
6
7/// The installed stdlib catalog, or the typed reason there is none.
8///
9/// With the `bundled-stdlib` feature the bundled catalog is installed on first
10/// read; otherwise the host must have called `harn_stdlib::install()`.
11pub fn try_stdlib_catalog() -> Result<&'static StdlibCatalog, CatalogNotInstalled> {
12    ensure_stdlib_installed()?;
13    harn_stdlib_catalog::installed()
14}
15
16/// Whether a stdlib catalog is installed, without building it.
17pub 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
23/// The installed stdlib catalog. Reading the stdlib without one is a host
24/// bug, so it panics with the typed reason instead of acting as if the
25/// standard library were empty.
26pub fn stdlib_catalog() -> &'static StdlibCatalog {
27    try_stdlib_catalog().unwrap_or_else(|error| panic!("{error}"))
28}
29
30/// Return the embedded stdlib source for `module` (the part after
31/// `std/`), or `None` if no stdlib module with that name exists.
32pub(crate) fn get_stdlib_source(module: &str) -> Option<&'static str> {
33    stdlib_catalog().source(module)
34}
35
36/// Rust builtins exported through an embedded stdlib module's public surface.
37pub(crate) fn builtin_reexports(module: &str) -> &'static [&'static str] {
38    harn_stdlib_catalog::builtin_reexports(module)
39}
40
41/// Sentinel path used to key embedded stdlib modules in the module
42/// graph. Real files never resolve to this path, so collisions are
43/// impossible.
44pub(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
52/// The canonical form of a virtual stdlib path, or `None` for an ordinary one.
53///
54/// Module-graph paths (`<std>/module`) are canonical as written. Every other
55/// embedded-stdlib spelling, such as the runtime's `<stdlib>/module.harn` or a
56/// virtual parent directory, cannot exist on disk: canonicalizing it always
57/// fails and is never memoized, so module sibling checks would call `realpath`
58/// on every import. It resolves lexically, exactly as that failure path does.
59pub(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
66/// Whether `path` lies under either virtual stdlib root: the module graph's
67/// `<std>` or the runtime's `<stdlib>`, including the roots themselves.
68pub(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
78/// Resolve a relative import owned by an embedded stdlib module.
79///
80/// Embedded modules use virtual paths rather than mirror files on disk. Keep
81/// their sibling-import semantics identical to ordinary modules while refusing
82/// any path that escapes the stdlib namespace.
83pub(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    /// The compiler crates read the stdlib through `harn-stdlib-catalog`. A
108    /// normal dependency on `harn-stdlib` would put every embedded `.harn` file
109    /// back into their build inputs, so one stdlib edit would rebuild them and
110    /// everything compiled against them again.
111    #[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}