Skip to main content

harn_stdlib_catalog/
lib.rs

1//! Registration point for the embedded Harn standard library.
2//!
3//! The compiler and runtime read stdlib sources through this crate instead of
4//! depending on `harn-stdlib`, which embeds every `.harn` source file. A crate
5//! that embeds the sources changes whenever one of them does, and so does
6//! everything compiled against it; keeping the catalog behind this small,
7//! dependency-free crate means editing a stdlib module rebuilds only
8//! `harn-stdlib` and the binaries that link it.
9//!
10//! A host links `harn-stdlib` and calls `harn_stdlib::install()` once at
11//! startup. Reading the catalog before that is a typed
12//! [`CatalogNotInstalled`] error, never an empty standard library.
13
14use std::sync::OnceLock;
15
16/// One embedded stdlib module, imported as `std/<module>`.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct StdlibSource {
19    pub module: &'static str,
20    pub source: &'static str,
21}
22
23/// One embedded prompt template, addressed by its path under `std/`.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct StdlibPromptAsset {
26    pub path: &'static str,
27    pub source: &'static str,
28}
29
30/// Embedded `.harn` script that backs a CLI subcommand. Looked up by
31/// the `harn-cli` dispatch wedge (see harn#2293 epic and harn#2294 G1)
32/// so subcommands can ship in Harn instead of Rust.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct StdlibCliScript {
35    /// Lookup name. For nested scripts this is the path under
36    /// `stdlib/cli/` without the `.harn` extension (e.g. `"eval/prompt"`
37    /// for `stdlib/cli/eval/prompt.harn`).
38    pub name: &'static str,
39    /// Embedded source. Run via the existing `harn run` codepath by the
40    /// dispatch wedge.
41    pub source: &'static str,
42}
43
44/// Every embedded stdlib source a host provides.
45#[derive(Debug)]
46pub struct StdlibCatalog {
47    pub sources: &'static [StdlibSource],
48    pub prompt_assets: &'static [StdlibPromptAsset],
49    pub cli_scripts: &'static [StdlibCliScript],
50}
51
52impl StdlibCatalog {
53    /// The source of `std/<module>`, or `None` when no such module exists.
54    pub fn source(&self, module: &str) -> Option<&'static str> {
55        self.sources
56            .iter()
57            .find_map(|entry| (entry.module == module).then_some(entry.source))
58    }
59
60    /// The prompt template at `path`, with or without its `std/` prefix.
61    pub fn prompt_asset(&self, path: &str) -> Option<&'static str> {
62        let path = path.strip_prefix("std/").unwrap_or(path);
63        self.prompt_assets
64            .iter()
65            .find_map(|entry| (entry.path == path).then_some(entry.source))
66    }
67
68    /// The CLI subcommand script registered under `name`.
69    pub fn cli_script(&self, name: &str) -> Option<&'static str> {
70        self.cli_scripts
71            .iter()
72            .find_map(|entry| (entry.name == name).then_some(entry.source))
73    }
74}
75
76static INSTALLED: OnceLock<&'static StdlibCatalog> = OnceLock::new();
77
78/// No stdlib catalog was installed before Harn code needed one.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct CatalogNotInstalled;
81
82impl std::fmt::Display for CatalogNotInstalled {
83    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        formatter.write_str(
85            "no Harn standard library is installed: the host must link harn-stdlib and call \
86             harn_stdlib::install() before running Harn code",
87        )
88    }
89}
90
91impl std::error::Error for CatalogNotInstalled {}
92
93/// A different catalog is already installed in this process.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub struct CatalogAlreadyInstalled;
96
97impl std::fmt::Display for CatalogAlreadyInstalled {
98    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        formatter.write_str("a different Harn standard library is already installed")
100    }
101}
102
103impl std::error::Error for CatalogAlreadyInstalled {}
104
105/// Install the process's stdlib catalog. Installing the same catalog again
106/// is a no-op; a second, different catalog is refused.
107pub fn install(catalog: &'static StdlibCatalog) -> Result<(), CatalogAlreadyInstalled> {
108    let installed = INSTALLED.get_or_init(|| catalog);
109    if std::ptr::eq(*installed, catalog) {
110        Ok(())
111    } else {
112        Err(CatalogAlreadyInstalled)
113    }
114}
115
116/// The installed catalog.
117pub fn installed() -> Result<&'static StdlibCatalog, CatalogNotInstalled> {
118    INSTALLED.get().copied().ok_or(CatalogNotInstalled)
119}
120
121/// Builtins a stdlib module re-exports under its own name, so
122/// `import { assert_eq } from "std/testing"` resolves.
123///
124/// Some of the standard library is implemented in Rust rather than Harn — the
125/// value differ behind `assert_eq` needs to walk the runtime representation of
126/// a value, which Harn source cannot do. Those builtins are callable without an
127/// import, but a reader who has just typed `import { assert_throws } from
128/// "std/testing"` has every reason to expect its sibling `assert_eq` to come
129/// from the same place, and no reason to know which side of the Rust/Harn line
130/// a given assertion happens to fall on. This table erases that seam: the
131/// module's export surface is its `pub fn`s plus the names listed here.
132///
133/// The list is explicit rather than derived from the builtins' `category`
134/// metadata: a category is a free-form label for docs and observability, and an
135/// export surface is an API contract. Deriving one from the other would let an
136/// unrelated metadata edit silently add or remove a public export.
137///
138/// `harn_vm::stdlib::tests` pins every name here to a registered builtin, so an
139/// entry cannot rot into a name that no longer exists.
140pub fn builtin_reexports(module: &str) -> &'static [&'static str] {
141    match module {
142        "testing" => &[
143            "assert",
144            "assert_approx",
145            "assert_eq",
146            "assert_matches",
147            "assert_ne",
148            "skip",
149            "value_diff",
150        ],
151        _ => &[],
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    static ONE: StdlibCatalog = StdlibCatalog {
160        sources: &[StdlibSource {
161            module: "text",
162            source: "pub fn shout() {}",
163        }],
164        prompt_assets: &[StdlibPromptAsset {
165            path: "agent/prompts/x.harn.prompt",
166            source: "hello",
167        }],
168        cli_scripts: &[],
169    };
170    static OTHER: StdlibCatalog = StdlibCatalog {
171        sources: &[],
172        prompt_assets: &[],
173        cli_scripts: &[],
174    };
175
176    #[test]
177    fn lookups_answer_by_module_and_by_prefixed_path() {
178        assert_eq!(ONE.source("text"), Some("pub fn shout() {}"));
179        assert_eq!(ONE.source("missing"), None);
180        assert_eq!(
181            ONE.prompt_asset("std/agent/prompts/x.harn.prompt"),
182            Some("hello")
183        );
184    }
185
186    #[test]
187    fn a_reader_before_install_gets_a_typed_error_then_the_one_catalog() {
188        assert_eq!(installed().err(), Some(CatalogNotInstalled));
189        install(&ONE).expect("first install");
190        install(&ONE).expect("same catalog again");
191        assert_eq!(install(&OTHER), Err(CatalogAlreadyInstalled));
192        assert!(std::ptr::eq(
193            installed().expect("installed"),
194            &raw const ONE
195        ));
196    }
197}