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::{LazyLock, 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
76/// A catalog built on its first read, so a host that installs one at startup
77/// pays for it only once Harn code needs it.
78pub type LazyCatalog = LazyLock<StdlibCatalog>;
79
80static INSTALLED: OnceLock<&'static LazyCatalog> = OnceLock::new();
81
82/// No stdlib catalog was installed before Harn code needed one.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct CatalogNotInstalled;
85
86impl std::fmt::Display for CatalogNotInstalled {
87    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        formatter.write_str(
89            "no Harn standard library is installed: the host must link harn-stdlib and call \
90             harn_stdlib::install() before running Harn code",
91        )
92    }
93}
94
95impl std::error::Error for CatalogNotInstalled {}
96
97/// A different catalog is already installed in this process.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub struct CatalogAlreadyInstalled;
100
101impl std::fmt::Display for CatalogAlreadyInstalled {
102    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        formatter.write_str("a different Harn standard library is already installed")
104    }
105}
106
107impl std::error::Error for CatalogAlreadyInstalled {}
108
109/// Install the process's stdlib catalog. Installing the same catalog again
110/// is a no-op; a second, different catalog is refused.
111pub fn install(catalog: &'static LazyCatalog) -> Result<(), CatalogAlreadyInstalled> {
112    let installed = INSTALLED.get_or_init(|| catalog);
113    if std::ptr::eq(*installed, catalog) {
114        Ok(())
115    } else {
116        Err(CatalogAlreadyInstalled)
117    }
118}
119
120/// Whether a catalog is installed, without building it. Startup checks use
121/// this so a command that never runs Harn code never pays for the catalog.
122pub fn ensure_installed() -> Result<(), CatalogNotInstalled> {
123    INSTALLED.get().map(|_| ()).ok_or(CatalogNotInstalled)
124}
125
126/// The installed catalog.
127pub fn installed() -> Result<&'static StdlibCatalog, CatalogNotInstalled> {
128    INSTALLED
129        .get()
130        .map(|catalog| &***catalog)
131        .ok_or(CatalogNotInstalled)
132}
133
134/// Builtins a stdlib module re-exports under its own name, so
135/// `import { assert_eq } from "std/testing"` resolves.
136///
137/// Some of the standard library is implemented in Rust rather than Harn — the
138/// value differ behind `assert_eq` needs to walk the runtime representation of
139/// a value, which Harn source cannot do. Those builtins are callable without an
140/// import, but a reader who has just typed `import { assert_throws } from
141/// "std/testing"` has every reason to expect its sibling `assert_eq` to come
142/// from the same place, and no reason to know which side of the Rust/Harn line
143/// a given assertion happens to fall on. This table erases that seam: the
144/// module's export surface is its `pub fn`s plus the names listed here.
145///
146/// The list is explicit rather than derived from the builtins' `category`
147/// metadata: a category is a free-form label for docs and observability, and an
148/// export surface is an API contract. Deriving one from the other would let an
149/// unrelated metadata edit silently add or remove a public export.
150///
151/// `harn_vm::stdlib::tests` pins every name here to a registered builtin, so an
152/// entry cannot rot into a name that no longer exists.
153pub fn builtin_reexports(module: &str) -> &'static [&'static str] {
154    match module {
155        "testing" => &[
156            "assert",
157            "assert_approx",
158            "assert_eq",
159            "assert_matches",
160            "assert_ne",
161            "skip",
162            "value_diff",
163        ],
164        _ => &[],
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    static ONE: LazyCatalog = LazyCatalog::new(|| StdlibCatalog {
173        sources: &[StdlibSource {
174            module: "text",
175            source: "pub fn shout() {}",
176        }],
177        prompt_assets: &[StdlibPromptAsset {
178            path: "agent/prompts/x.harn.prompt",
179            source: "hello",
180        }],
181        cli_scripts: &[],
182    });
183    static OTHER: LazyCatalog = LazyCatalog::new(|| StdlibCatalog {
184        sources: &[],
185        prompt_assets: &[],
186        cli_scripts: &[],
187    });
188
189    #[test]
190    fn lookups_answer_by_module_and_by_prefixed_path() {
191        assert_eq!(ONE.source("text"), Some("pub fn shout() {}"));
192        assert_eq!(ONE.source("missing"), None);
193        assert_eq!(
194            ONE.prompt_asset("std/agent/prompts/x.harn.prompt"),
195            Some("hello")
196        );
197    }
198
199    static BUILDS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
200    static COUNTED: LazyCatalog = LazyCatalog::new(|| {
201        BUILDS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
202        StdlibCatalog {
203            sources: &[],
204            prompt_assets: &[],
205            cli_scripts: &[],
206        }
207    });
208
209    #[test]
210    fn a_reader_before_install_gets_a_typed_error_then_the_one_catalog() {
211        assert_eq!(installed().err(), Some(CatalogNotInstalled));
212        assert_eq!(ensure_installed(), Err(CatalogNotInstalled));
213        install(&COUNTED).expect("first install");
214        install(&COUNTED).expect("same catalog again");
215        assert_eq!(install(&OTHER), Err(CatalogAlreadyInstalled));
216        // Startup checks must not decode the catalog: `harn --version` does one.
217        ensure_installed().expect("installed");
218        assert_eq!(BUILDS.load(std::sync::atomic::Ordering::SeqCst), 0);
219        let counted: &StdlibCatalog = &COUNTED;
220        assert!(std::ptr::eq(installed().expect("installed"), counted));
221        assert_eq!(BUILDS.load(std::sync::atomic::Ordering::SeqCst), 1);
222    }
223}