Skip to main content

outl_exec/
registry.rs

1//! Map from fence info-string (`"lisp"`, `"python"`, ...) to runtime.
2//!
3//! Built-in runtimes are registered via [`RuntimeRegistry::default`].
4//! Hosts can layer more at startup with [`RuntimeRegistry::register`],
5//! or discover drop-in `.wasm` modules with
6//! `RuntimeRegistry::discover_wasm_dir` (M2 — see TODO inside).
7
8use std::collections::HashMap;
9use std::sync::Arc;
10
11use crate::runtime::Runtime;
12use crate::runtimes;
13
14/// Owned set of runtimes, keyed by the lowercased fence info-string.
15#[derive(Clone, Default)]
16pub struct RuntimeRegistry {
17    by_lang: HashMap<String, Arc<dyn Runtime>>,
18}
19
20impl RuntimeRegistry {
21    /// Empty registry. Most callers want [`RuntimeRegistry::with_builtins`].
22    pub fn new() -> Self {
23        Self {
24            by_lang: HashMap::new(),
25        }
26    }
27
28    /// New registry pre-populated with every shipped runtime.
29    ///
30    /// Each language ships behind a feature (`lang-lisp`, `lang-js`,
31    /// `lang-python`, `lang-lua`) so binaries can strip what they don't
32    /// need. `echo` is the smoke-test runtime — gated behind
33    /// `debug_assertions` / `test` so release builds (notably the iOS
34    /// IPA) don't surface an "echo" language to the App Store
35    /// reviewer; it has no production value and reads as a dev hook.
36    pub fn with_builtins() -> Self {
37        let mut r = Self::new();
38        #[cfg(any(test, debug_assertions))]
39        r.register(runtimes::echo::EchoRuntime);
40        #[cfg(feature = "lang-lisp")]
41        r.register(runtimes::lisp::LispRuntime);
42        #[cfg(feature = "lang-js")]
43        r.register(runtimes::js::JsRuntime);
44        #[cfg(feature = "lang-python")]
45        r.register(runtimes::python::PythonRuntime);
46        #[cfg(feature = "lang-lua")]
47        r.register(runtimes::lua::LuaRuntime);
48        #[cfg(feature = "lang-rust")]
49        r.register(runtimes::rust::RustRuntime::default());
50        #[cfg(feature = "lang-query")]
51        r.register(runtimes::query::QueryRuntime);
52        r
53    }
54
55    /// Insert (or replace) a runtime. The lookup key is the runtime's
56    /// own `language()`, lowercased.
57    pub fn register<R: Runtime + 'static>(&mut self, r: R) -> &mut Self {
58        self.by_lang
59            .insert(r.language().to_ascii_lowercase(), Arc::new(r));
60        self
61    }
62
63    /// Look up a runtime by fence info-string. Returns `None` if no
64    /// runtime is registered for that language.
65    ///
66    /// The input is run through [`outl_md::lang::canonical`] first
67    /// so user-written aliases (`javascript`, `node`, `rs`, `py3`,
68    /// …) resolve to the registry key the runtime registered with
69    /// (`js`, `rust`, `python`). The original lower-cased form is
70    /// the fallback when no alias matches — keeps the door open for
71    /// runtimes registered out-of-band that don't have an alias
72    /// entry yet.
73    pub fn get(&self, lang: &str) -> Option<Arc<dyn Runtime>> {
74        let key = outl_md::lang::canonical(lang)
75            .map(str::to_owned)
76            .unwrap_or_else(|| lang.to_ascii_lowercase());
77        self.by_lang.get(&key).cloned()
78    }
79
80    /// Every registered language. Useful for `:run ?` style help.
81    pub fn languages(&self) -> impl Iterator<Item = &str> {
82        self.by_lang.keys().map(String::as_str)
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn default_registers_echo() {
92        let r = RuntimeRegistry::with_builtins();
93        assert!(r.get("echo").is_some());
94    }
95
96    #[cfg(feature = "lang-lisp")]
97    #[test]
98    fn registers_lisp_when_feature_on() {
99        let r = RuntimeRegistry::with_builtins();
100        assert!(r.get("lisp").is_some());
101    }
102
103    #[test]
104    fn lookup_is_case_insensitive() {
105        let r = RuntimeRegistry::with_builtins();
106        assert!(r.get("ECHO").is_some());
107        assert!(r.get("Echo").is_some());
108    }
109
110    #[test]
111    fn unknown_language_returns_none() {
112        let r = RuntimeRegistry::with_builtins();
113        assert!(r.get("klingon").is_none());
114    }
115}