Skip to main content

hara_native/runtime/
model.rs

1/// Marks every namespace already materialized by the runtime bootstrap as loaded.
2///
3/// Embedding hosts receive only the namespace and protocol registries, not the
4/// Runtime's source-provider state. A source fallback can therefore leave a
5/// materialized Foundation namespace recorded as `Unloaded` even though all of
6/// its Vars are present. Normalizing only materialized namespaces preserves
7/// lazy package discovery while making the exported registry self-contained.
8fn normalize_embedding_namespace_load_states(namespaces: &kernel::NamespaceRegistry<core::Value>) {
9    for namespace in namespaces.all() {
10        namespaces.set_load_state(
11            namespace.name().as_str(),
12            kernel::NamespaceLoadState::Loaded,
13        );
14    }
15}
16
17/// Builds the fully bootstrapped namespace registry used by native embedding hosts.
18///
19/// Hosts receive the same Foundation Vars, primitive values and protocol wiring
20/// as a normal Hara runtime without depending on crate-private bootstrap helpers.
21pub fn embedding_namespace_registry() -> kernel::NamespaceRegistry<core::Value> {
22    let namespaces = Runtime::new().namespace_registry.clone();
23    normalize_embedding_namespace_load_states(&namespaces);
24    namespaces
25}
26
27#[cfg(test)]
28mod embedding_namespace_tests {
29    use super::*;
30
31    #[test]
32    fn normalization_marks_only_materialized_namespaces_loaded() {
33        let namespaces = kernel::NamespaceRegistry::<core::Value>::new("user");
34        namespaces.find_or_create("std.foundation");
35        namespaces.set_load_state("std.foundation", kernel::NamespaceLoadState::Unloaded);
36        namespaces.set_load_state("example.lazy", kernel::NamespaceLoadState::Unloaded);
37
38        normalize_embedding_namespace_load_states(&namespaces);
39
40        assert_eq!(
41            namespaces.load_state("std.foundation"),
42            Some(kernel::NamespaceLoadState::Loaded)
43        );
44        assert_eq!(
45            namespaces.load_state("example.lazy"),
46            Some(kernel::NamespaceLoadState::Unloaded)
47        );
48    }
49
50    #[test]
51    fn exported_registry_satisfies_bootstrap_requires_without_source_provider() {
52        let namespaces = embedding_namespace_registry();
53        let form = kernel::parse_forms(
54            "(ns example.embedding (:require [std.foundation :refer :all] [std.foundation.coroutine :as coroutine]))",
55        )
56        .expect("parse embedding namespace declaration")
57        .into_iter()
58        .next()
59        .expect("embedding namespace declaration");
60        let mut environment = std::collections::HashMap::new();
61
62        core::with_namespace_registry(&namespaces, || core::eval(&form, &mut environment))
63            .expect("embedding registry must satisfy bootstrapped requires");
64
65        assert_eq!(
66            namespaces.load_state("std.foundation"),
67            Some(kernel::NamespaceLoadState::Loaded)
68        );
69        assert_eq!(
70            namespaces.load_state("std.foundation.coroutine"),
71            Some(kernel::NamespaceLoadState::Loaded)
72        );
73        assert!(namespaces.find("example.embedding").is_some());
74    }
75}
76
77#[cfg(not(feature = "raw-wasm"))]
78use wasm_bindgen::prelude::*;
79
80include!(concat!(env!("OUT_DIR"), "/embedded_hal.rs"));
81
82const EAGER_HAL_RESOURCES: &[&str] = &[
83    "std.foundation.string",
84    "std.foundation.promise",
85    "std.foundation.bytes",
86    "std.foundation.coroutine",
87    "std.foundation.pretty",
88    "std.stream.duplex",
89];
90
91fn ignore_socket_event(_event: core::SocketEvent) {}
92
93#[cfg(not(feature = "raw-wasm"))]
94#[wasm_bindgen(start)]
95pub fn init_wasm() {
96    #[cfg(target_arch = "wasm32")]
97    console_error_panic_hook::set_once();
98}
99
100/// Runs the shared instrumentation corpus inside the browser/Wasm runtime.
101///
102/// The returned report is produced by the runtime-owned instrumentation hub,
103/// not by JavaScript projection logic. Hosts can therefore compare repeated
104/// browser runs with the native Rust and Java reports byte-for-byte.
105#[cfg(not(feature = "raw-wasm"))]
106#[wasm_bindgen]
107pub fn instrumentation_conformance(corpus: &str) -> Result<String, JsValue> {
108    let corpus: serde_json::Value =
109        serde_json::from_str(corpus).map_err(|error| JsValue::from_str(&error.to_string()))?;
110    let report = crate::instrumentation::conformance::report(&corpus, "wasm")
111        .map_err(|error| JsValue::from_str(&error))?;
112    serde_json::to_string_pretty(&report).map_err(|error| JsValue::from_str(&error.to_string()))
113}
114
115#[cfg_attr(not(feature = "raw-wasm"), wasm_bindgen)]
116pub struct PromiseHandle {
117    promise: core::Promise,
118}
119
120#[cfg_attr(not(feature = "raw-wasm"), wasm_bindgen)]
121impl PromiseHandle {
122    fn from_promise(promise: core::Promise) -> PromiseHandle {
123        PromiseHandle { promise }
124    }
125
126    #[cfg_attr(not(feature = "raw-wasm"), wasm_bindgen(constructor))]
127    pub fn new() -> PromiseHandle {
128        PromiseHandle {
129            promise: core::Promise::new(),
130        }
131    }
132
133    pub fn state(&self) -> String {
134        match self.promise.state() {
135            core::PromiseState::Pending => "pending".into(),
136            core::PromiseState::Fulfilled(_) => "fulfilled".into(),
137            core::PromiseState::Rejected(_) => "rejected".into(),
138        }
139    }
140
141    pub fn resolve(&self, value: &str) -> bool {
142        self.promise.resolve(core::Value::String(value.into()))
143    }
144
145    pub fn reject(&self, error: &str) -> bool {
146        self.promise.reject(error)
147    }
148
149    pub fn adopt(&self, other: &PromiseHandle) -> bool {
150        self.promise.adopt(&other.promise)
151    }
152
153    pub fn value(&self) -> Result<String, JsValue> {
154        match self.promise.state() {
155            core::PromiseState::Pending => Err(JsValue::from_str("promise is pending")),
156            core::PromiseState::Fulfilled(value) => Ok(value.display()),
157            core::PromiseState::Rejected(error) => Err(JsValue::from_str(&error.message())),
158        }
159    }
160}
161
162#[cfg_attr(not(feature = "raw-wasm"), wasm_bindgen)]
163pub struct Runtime {
164    execution: RuntimeExecutionState,
165    test_runner: String,
166    execution_backend: String,
167    protocols: core::ProtocolRegistry,
168    extensions: core::ExtensionRegistry,
169    wasm_extensions: HashMap<String, extension::WasmExtension>,
170    native_wasm_imports: HashMap<String, extension::WasmExtension>,
171    providers: core::ProviderRegistry,
172    package_catalog: core::PackageCatalog,
173    resources: HashMap<String, String>,
174    #[cfg(not(target_arch = "wasm32"))]
175    source_paths: HashMap<String, std::path::PathBuf>,
176    resource_overrides: HashSet<String>,
177    #[cfg(feature = "bytecode-vm")]
178    bytecode_resources: HashMap<String, (String, Vec<u8>)>,
179    product_cache: RefCell<compiled_product::InMemoryProductCache>,
180    loaded_resources: HashSet<String>,
181    halc_schema_definitions: HashMap<String, Form>,
182    halc_function_schemas: HashMap<String, Form>,
183    halc_schema_types: HashMap<String, kernel::SchemaType>,
184    halc_function_types: HashMap<String, kernel::SchemaType>,
185    halc_inferred_function_types: HashMap<String, kernel::SchemaType>,
186    namespace_registry: kernel::NamespaceRegistry<core::Value>,
187    macros: Rc<RefCell<HashMap<(String, String), Rc<core::Function>>>>,
188    generated_configs: HashMap<String, kernel::GeneratedNamespaceConfig>,
189    #[cfg(feature = "evaluation-journal")]
190    next_journal_id: u64,
191    #[cfg(all(target_arch = "wasm32", not(feature = "raw-wasm")))]
192    host_handler: Option<js_sys::Function>,
193    #[cfg(not(target_arch = "wasm32"))]
194    native_host_handler:
195        Option<Rc<dyn Fn(String, String, Vec<core::Value>) -> Result<core::Value, String>>>,
196    #[cfg(not(target_arch = "wasm32"))]
197    native_modules: native_module::Registry,
198    #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
199    direct_native: crate::direct_native::NativeEngine,
200    #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
201    direct_native_multimethods: core::MultiMethodRegistry,
202    #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
203    direct_native_source_cache: Option<SourceBytecodeCache>,
204    #[cfg(not(target_arch = "wasm32"))]
205    extension_roots: Vec<std::path::PathBuf>,
206}
207
208impl Drop for Runtime {
209    fn drop(&mut self) {
210        // Namespace vars and the flattened environment retain native export
211        // closures, and those closures retain the extension session. Release
212        // the bindings before dropping the session owners so provider
213        // shutdown is deterministic at the Runtime boundary.
214        let namespaces = self.wasm_extensions.keys().cloned().collect::<Vec<_>>();
215        for namespace in self.namespace_registry.all() {
216            for (symbol, var) in namespace.mappings() {
217                if var
218                    .symbol()
219                    .get_namespace()
220                    .is_some_and(|owner| namespaces.iter().any(|extension| extension == owner))
221                {
222                    namespace.unmap(&symbol);
223                }
224            }
225        }
226        self.execution.clear();
227        #[cfg(not(target_arch = "wasm32"))]
228        self.native_wasm_imports.clear();
229        self.wasm_extensions.clear();
230    }
231}