Skip to main content

harn_vm/vm/
modules.rs

1use std::collections::BTreeMap;
2use std::future::Future;
3use std::hash::{Hash, Hasher};
4use std::path::{Path, PathBuf};
5use std::pin::Pin;
6use std::sync::{Arc, Mutex, OnceLock};
7
8use harn_modules::DefKind;
9
10use crate::bytecode_cache;
11use crate::module_artifact::compile_module_artifact_from_source;
12use crate::prepared_module::PreparedModuleArtifact;
13use crate::value::{ModuleFunctionRegistry, VmClosure, VmEnv, VmError, VmValue};
14
15use super::{ScopeSpan, Vm};
16
17static STDLIB_MODULE_ARTIFACT_CACHE: OnceLock<
18    Mutex<BTreeMap<String, Arc<PreparedModuleArtifact>>>,
19> = OnceLock::new();
20
21fn stdlib_module_artifact_cache() -> &'static Mutex<BTreeMap<String, Arc<PreparedModuleArtifact>>> {
22    STDLIB_MODULE_ARTIFACT_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
23}
24
25fn verified_package_source(bytes: Vec<u8>, path: &Path) -> Result<String, VmError> {
26    String::from_utf8(bytes).map_err(|error| {
27        VmError::Runtime(format!(
28            "installed package source {} is not valid UTF-8: {error}",
29            path.display()
30        ))
31    })
32}
33
34fn exported_function_closures(
35    loaded: &LoadedModule,
36    display_path: &Path,
37) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
38    let mut exports = BTreeMap::new();
39    for name in loaded
40        .public_exports
41        .keys()
42        .filter(|name| loaded.functions.contains_key(*name))
43    {
44        let Some(closure) = loaded.functions.get(name) else {
45            return Err(VmError::Runtime(format!(
46                "Import error: exported function '{name}' is missing from {}",
47                display_path.display()
48            )));
49        };
50        exports.insert(name.clone(), Arc::clone(closure));
51    }
52    Ok(exports)
53}
54
55#[cfg(test)]
56fn reset_stdlib_module_artifact_cache() {
57    stdlib_module_artifact_cache().lock().unwrap().clear();
58}
59
60#[cfg(test)]
61fn stdlib_module_artifact_cache_ptr(module: &str, source: &str) -> Option<usize> {
62    let key = stdlib_artifact_cache_key(module, source);
63    stdlib_module_artifact_cache()
64        .lock()
65        .unwrap()
66        .get(&key)
67        .map(|artifact| Arc::as_ptr(artifact) as usize)
68}
69
70pub(crate) struct LoadedModule {
71    pub(crate) functions: BTreeMap<String, Arc<VmClosure>>,
72    /// Shared public declaration contract copied from the artifact and
73    /// extended by explicit re-exports.
74    pub(crate) public_exports: BTreeMap<String, DefKind>,
75    /// Evaluated values of exported declarations whose runtime binding is
76    /// produced by module initialization, including structs and enums.
77    pub(crate) public_values: BTreeMap<String, VmValue>,
78    /// Decoded JSON-Schema dict for each `pub type` alias that lowers to a
79    /// schema. Importers bind the alias name to this value so
80    /// expression-position uses (`output: ImportedAlias`) work.
81    pub(crate) public_type_schemas: BTreeMap<String, VmValue>,
82    /// Guard under which this filesystem module and its transitive closure were
83    /// instantiated. A guarded execution cannot reuse an unguarded module even
84    /// when the entry bytes currently match: its closures may retain imports
85    /// compiled from earlier, unverified bytes.
86    package_execution_guard: Option<Arc<harn_modules::package_execution::PackageExecutionGuard>>,
87    pub(crate) _module_functions: crate::value::ModuleFunctionRegistry,
88    pub(crate) _module_state: crate::value::ModuleState,
89}
90
91/// Runtime module cache shared by child VMs within one execution tree.
92///
93/// The map stays copy-on-write so a child can add modules without mutating its
94/// parent. Cache entries are never replaced after instantiation, so cache hits
95/// and map copies share their export maps plus their existing shared
96/// registries/state through a cheap outer [`Arc`] instead of cloning the whole
97/// module.
98pub(crate) type ModuleCache = Arc<BTreeMap<PathBuf, Arc<LoadedModule>>>;
99
100/// An import whose target module was still mid-load (an import cycle) when the
101/// importing module reached it. The target's function closures don't exist yet
102/// at that point, so the binding can't happen inline. We record it here and
103/// resolve it once both modules are fully loaded — see
104/// [`Vm::flush_deferred_cyclic_imports`].
105#[derive(Clone, Debug)]
106pub(crate) struct DeferredCyclicImport {
107    /// Canonical path of the module that issued the import.
108    pub(crate) importer: PathBuf,
109    /// Canonical path of the cyclically-imported target module.
110    pub(crate) target: PathBuf,
111    /// Selectively-imported names, or `None` for a wildcard/side-effect import.
112    pub(crate) selected_names: Option<Vec<String>>,
113    /// When set, bind a namespace dict under this alias instead of flattening.
114    pub(crate) namespace_alias: Option<String>,
115}
116
117#[derive(Clone, Copy)]
118enum ImportProjection<'a> {
119    BindCaller(Option<&'a [String]>),
120    /// Bind `import * as alias` as a single namespace dict.
121    BindNamespace(&'a str),
122    MaterializeOnly,
123}
124
125impl ImportProjection<'_> {
126    fn package_rejection_kind(self) -> &'static str {
127        match self {
128            Self::BindCaller(_) | Self::BindNamespace(_) => "import",
129            Self::MaterializeOnly => "execution",
130        }
131    }
132}
133
134/// Resolve the names an import may introduce from one loaded module. The
135/// artifact's typed export contract is authoritative for ordinary imports,
136/// re-exports, and delayed cycle binding alike.
137fn module_import_names(
138    module_name: &str,
139    loaded: &LoadedModule,
140    selected_names: Option<&[String]>,
141) -> Result<Vec<String>, VmError> {
142    if let Some(names) = selected_names {
143        for name in names {
144            if !loaded.public_exports.contains_key(name) {
145                let hint = if loaded.functions.contains_key(name) {
146                    " — it is defined there but not `pub`; mark it `pub` to export it"
147                } else {
148                    ""
149                };
150                return Err(VmError::Runtime(format!(
151                    "Import error: '{name}' is not exported by {module_name}{hint}"
152                )));
153            }
154        }
155        return Ok(names.to_vec());
156    }
157
158    Ok(loaded.public_exports.keys().cloned().collect())
159}
160
161/// Build the closed namespace dict for `import * as alias from path`.
162///
163/// Includes `"_namespace" → module path` so `call_dict_method` dispatches
164/// callable fields, plus every public export that has a runtime value.
165/// Type/interface-only exports are omitted (no runtime binding).
166fn build_namespace_dict(module_path: &str, loaded: &LoadedModule) -> VmValue {
167    let mut map = BTreeMap::new();
168    map.insert(
169        "_namespace".to_string(),
170        VmValue::String(arcstr::ArcStr::from(module_path)),
171    );
172    for (name, kind) in &loaded.public_exports {
173        if !kind.has_runtime_value() {
174            // Still project schema-capable type aliases when present.
175            if let Some(schema) = loaded.public_type_schemas.get(name) {
176                map.insert(name.clone(), schema.clone());
177            }
178            continue;
179        }
180        if let Some(value) = loaded.public_values.get(name) {
181            map.insert(name.clone(), value.clone());
182            continue;
183        }
184        if let Some(schema) = loaded.public_type_schemas.get(name) {
185            map.insert(name.clone(), schema.clone());
186            continue;
187        }
188        if let Some(closure) = loaded.functions.get(name) {
189            map.insert(name.clone(), VmValue::Closure(Arc::clone(closure)));
190        }
191    }
192    VmValue::dict(map)
193}
194
195pub fn resolve_module_import_path(base: &Path, path: &str) -> PathBuf {
196    let synthetic_current_file = base.join("__harn_import_base__.harn");
197    if let Some(resolved) = harn_modules::resolve_import_path(&synthetic_current_file, path) {
198        return resolved;
199    }
200
201    let mut file_path = base.join(path);
202
203    if !file_path.exists() && file_path.extension().is_none() {
204        file_path.set_extension("harn");
205    }
206
207    file_path
208}
209
210fn stdlib_artifact_cache_key(module: &str, source: &str) -> String {
211    let mut hasher = std::collections::hash_map::DefaultHasher::new();
212    module.hash(&mut hasher);
213    source.hash(&mut hasher);
214    format!("{module}:{:016x}", hasher.finish())
215}
216
217fn stdlib_module_artifact(
218    module: &str,
219    synthetic: &Path,
220    source: &'static str,
221    recorder: Option<&super::ModulePhaseRecorder>,
222) -> Result<Arc<PreparedModuleArtifact>, VmError> {
223    let key = stdlib_artifact_cache_key(module, source);
224    {
225        let cache = stdlib_module_artifact_cache().lock().unwrap();
226        if let Some(cached) = cache.get(&key) {
227            return Ok(Arc::clone(cached));
228        }
229    }
230
231    // Stdlib modules are embedded in the binary so their content cannot
232    // legitimately change between processes; that means the disk cache
233    // for stdlib can use a synthetic source_path. The harn_version field
234    // of the cache key gates correctness across releases.
235    let lookup = {
236        let _load_span = recorder.map(super::ModulePhaseRecorder::load_span);
237        bytecode_cache::load_module(synthetic, source)
238    };
239    let artifact = if let Some(artifact) = lookup.artifact {
240        artifact
241    } else {
242        let mut compile_span = recorder.map(super::ModulePhaseRecorder::compile_span);
243        let compiled = compile_module_artifact_from_source(synthetic, source)?;
244        if let Some(span) = &mut compile_span {
245            span.mark_compile_succeeded();
246        }
247        drop(compile_span);
248        if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
249            if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
250                eprintln!("[harn] stdlib module cache write skipped for {module}: {err}");
251            }
252        }
253        compiled
254    };
255
256    let compiled = {
257        let _load_span = recorder.map(super::ModulePhaseRecorder::load_span);
258        Arc::new(PreparedModuleArtifact::from_cached(artifact))
259    };
260    let mut cache = stdlib_module_artifact_cache().lock().unwrap();
261    if let Some(cached) = cache.get(&key) {
262        return Ok(Arc::clone(cached));
263    }
264    cache.insert(key, Arc::clone(&compiled));
265    Ok(compiled)
266}
267
268impl Vm {
269    fn resolve_module_import_path(&self, base: &Path, path: &str) -> Result<PathBuf, VmError> {
270        if let Some(guard) = &self.package_execution_guard {
271            let synthetic_current_file = base.join("__harn_import_base__.harn");
272            if let Some(resolved) =
273                harn_modules::resolve_import_path_with_guard(&synthetic_current_file, path, guard)
274                    .map_err(|error| {
275                    VmError::Runtime(format!("installed package import rejected: {error}"))
276                })?
277            {
278                return Ok(resolved);
279            }
280            let mut file_path = base.join(path);
281            if !file_path.exists() && file_path.extension().is_none() {
282                file_path.set_extension("harn");
283            }
284            return Ok(file_path);
285        }
286        Ok(resolve_module_import_path(base, path))
287    }
288
289    /// Resolve a callable against this VM. Lazy callables initialize once per
290    /// VM execution tree, then retain that module scope for later child VMs in
291    /// the same tree. Fresh VM roots remain isolated.
292    pub async fn resolve_callable(
293        &mut self,
294        callable: &crate::value::VmCallable,
295    ) -> Result<Arc<crate::value::VmClosure>, VmError> {
296        self.ensure_execution_available()?;
297        match callable {
298            crate::value::VmCallable::Eager(closure) => Ok(Arc::clone(closure)),
299            crate::value::VmCallable::Lazy(lazy) => {
300                let (cache_key, module_path) = self.lazy_callable_module_path(lazy);
301                let next_guard = lazy
302                    .package_execution_guard_handle()
303                    .or_else(|| self.package_execution_guard.clone());
304                if let Some(guard) = &next_guard {
305                    guard.verify_entry_source(&module_path).map_err(|error| {
306                        VmError::Runtime(format!("installed package execution rejected: {error}"))
307                    })?;
308                }
309                let resolution = {
310                    let mut modules = self.lazy_callable_modules.lock();
311                    let slots = modules.entry(cache_key).or_default();
312                    if let Some(slot) = slots.iter().find(|slot| slot.execution_guard == next_guard)
313                    {
314                        Arc::clone(&slot.resolution)
315                    } else {
316                        let resolution = Arc::new(tokio::sync::OnceCell::new());
317                        slots.push(crate::vm::state::LazyCallableCacheSlot {
318                            execution_guard: next_guard.clone(),
319                            resolution: Arc::clone(&resolution),
320                        });
321                        resolution
322                    }
323                };
324                let previous_package_execution_guard =
325                    std::mem::replace(&mut self.package_execution_guard, next_guard);
326                let resolved = resolution
327                    .get_or_try_init(|| async {
328                        let exports = self.load_module_exports(&module_path).await?;
329                        let exports = exports
330                            .into_iter()
331                            .map(|(name, closure)| (name, closure.retained_for_host_registry()))
332                            .collect();
333                        // Pin the complete module graph loaded above so that a
334                        // handler's transitively imported callees keep their
335                        // home-module registries/state alive for later child
336                        // VMs that hit this cache without re-importing.
337                        Ok::<_, VmError>(Arc::new(crate::vm::state::ResolvedLazyCallable {
338                            exports,
339                            retained_module_graph: Arc::clone(&self.module_cache),
340                        }))
341                    })
342                    .await;
343                self.package_execution_guard = previous_package_execution_guard;
344                let resolved = resolved?;
345                resolved
346                    .exports
347                    .get(&lazy.function_name)
348                    .cloned()
349                    .ok_or_else(|| {
350                        VmError::Runtime(format!(
351                            "function '{}' is not exported by module '{}'",
352                            lazy.function_name,
353                            lazy.module_path.display()
354                        ))
355                    })
356            }
357            crate::value::VmCallable::Pipeline(_) => Err(VmError::TypeError(
358                "pipeline callable requires execute_callable".to_string(),
359            )),
360        }
361    }
362
363    pub async fn execute_callable(
364        &mut self,
365        callable: &crate::value::VmCallable,
366        args: &[crate::value::VmValue],
367    ) -> Result<crate::value::VmValue, VmError> {
368        let crate::value::VmCallable::Pipeline(pipeline) = callable else {
369            let closure = self.resolve_callable(callable).await?;
370            return self.call_closure_pub(&closure, args).await;
371        };
372
373        let (_, module_path) = self.lazy_module_path(&pipeline.module_path);
374        let next_guard = pipeline
375            .package_execution_guard_handle()
376            .or_else(|| self.package_execution_guard.clone());
377        let previous_package_execution_guard =
378            std::mem::replace(&mut self.package_execution_guard, next_guard);
379        let result = async {
380            let closure = self
381                .load_public_module_callable(&module_path, &pipeline.pipeline_name)
382                .await?;
383            self.call_closure_pub(&closure, args).await
384        }
385        .await;
386        self.package_execution_guard = previous_package_execution_guard;
387        result
388    }
389
390    fn lazy_callable_module_path(&self, lazy: &crate::value::LazyVmCallable) -> (PathBuf, PathBuf) {
391        self.lazy_module_path(&lazy.module_path)
392    }
393
394    fn lazy_module_path(&self, path: &std::path::Path) -> (PathBuf, PathBuf) {
395        let mut module_path = if path.is_absolute() {
396            path.to_path_buf()
397        } else {
398            self.source_dir
399                .clone()
400                .unwrap_or_else(|| PathBuf::from("."))
401                .join(path)
402        };
403        if !module_path.exists() && module_path.extension().is_none() {
404            module_path.set_extension("harn");
405        }
406        let cache_key = module_path
407            .canonicalize()
408            .unwrap_or_else(|_| module_path.clone());
409        (cache_key, module_path)
410    }
411
412    async fn load_module_from_source(
413        &mut self,
414        synthetic: PathBuf,
415        source: &str,
416    ) -> Result<Arc<LoadedModule>, VmError> {
417        if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
418            return Ok(loaded);
419        }
420        Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
421
422        let mut compile_span = self.module_compile_span();
423        let compiled = compile_module_artifact_from_source(&synthetic, source)?;
424        if let Some(span) = &mut compile_span {
425            span.mark_compile_succeeded();
426        }
427        drop(compile_span);
428        let artifact = {
429            let _load_span = self.module_load_span();
430            PreparedModuleArtifact::from_cached(compiled)
431        };
432
433        self.imported_paths.push(synthetic.clone());
434        let loaded = Arc::new(self.instantiate_module(None, &artifact).await?);
435        self.imported_paths.pop();
436        {
437            let _load_span = self.module_load_span();
438            Arc::make_mut(&mut self.module_cache).insert(synthetic, Arc::clone(&loaded));
439        }
440        self.record_module_loaded();
441        Ok(loaded)
442    }
443
444    /// Widen a stdlib module's export surface with the builtins it re-exports
445    /// (see [`harn_stdlib::builtin_reexports`]), so a Rust-implemented member of
446    /// the module imports exactly like a Harn-implemented one.
447    ///
448    /// The name binds to a [`VmValue::BuiltinRef`], which is what a bare mention
449    /// of a builtin already evaluates to — so an imported `assert_eq` and a
450    /// global `assert_eq` are the same function reached two ways, not two
451    /// implementations that can drift.
452    fn add_builtin_reexports(module: &str, loaded: &mut LoadedModule) {
453        for name in harn_stdlib::builtin_reexports(module) {
454            // A `pub fn` in the module's Harn source wins: it is the more
455            // specific declaration, and silently shadowing it here would make
456            // the source of an export unguessable from reading the module.
457            if loaded.public_exports.contains_key(*name) {
458                continue;
459            }
460            loaded
461                .public_exports
462                .insert((*name).to_string(), DefKind::Function);
463            loaded.public_values.insert(
464                (*name).to_string(),
465                VmValue::BuiltinRef(arcstr::ArcStr::from(*name)),
466            );
467        }
468    }
469
470    async fn load_stdlib_module_from_source(
471        &mut self,
472        module: &str,
473        synthetic: PathBuf,
474        source: &'static str,
475    ) -> Result<Arc<LoadedModule>, VmError> {
476        if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
477            return Ok(loaded);
478        }
479        Arc::make_mut(&mut self.source_cache).insert(synthetic.clone(), source.to_string());
480
481        let artifact = stdlib_module_artifact(
482            module,
483            &synthetic,
484            source,
485            self.module_phase_recorder.as_ref(),
486        )?;
487        self.imported_paths.push(synthetic.clone());
488        let mut loaded = self.instantiate_stdlib_module(artifact.as_ref()).await?;
489        self.imported_paths.pop();
490        Self::add_builtin_reexports(module, &mut loaded);
491        let loaded = Arc::new(loaded);
492        {
493            let _load_span = self.module_load_span();
494            Arc::make_mut(&mut self.module_cache).insert(synthetic, Arc::clone(&loaded));
495        }
496        self.record_module_loaded();
497        Ok(loaded)
498    }
499
500    async fn instantiate_stdlib_module(
501        &mut self,
502        artifact: &PreparedModuleArtifact,
503    ) -> Result<LoadedModule, VmError> {
504        self.instantiate_module(None, artifact).await
505    }
506
507    /// Instantiate a previously-hydrated [`PreparedModuleArtifact`] into a
508    /// [`LoadedModule`]. Re-runs nested imports, replays the init chunk
509    /// into a fresh module env, mints a [`VmClosure`] for each compiled
510    /// function (stamped with `module_source_dir` so imports from inside
511    /// those functions resolve against the originating file), and
512    /// applies the re-export pass. Used by both stdlib and user-import
513    /// code paths.
514    async fn instantiate_module(
515        &mut self,
516        module_source_dir: Option<PathBuf>,
517        artifact: &PreparedModuleArtifact,
518    ) -> Result<LoadedModule, VmError> {
519        let caller_env = self.env.clone();
520        let old_source_dir = self.source_dir.clone();
521        self.env = VmEnv::new();
522        self.source_dir = module_source_dir.clone();
523
524        for import in &artifact.imports {
525            if let Some(alias) = &import.namespace_alias {
526                self.execute_namespace_import_bind(&import.path, alias)
527                    .await?;
528            } else {
529                self.execute_import(&import.path, import.selected_names.as_deref())
530                    .await?;
531            }
532        }
533
534        // Nested modules own their own load spans. Start this module's span
535        // only after those imports finish so aggregate load time is additive.
536        let _load_span = self.module_load_span();
537
538        let module_state: crate::value::ModuleState = {
539            let mut init_env = self.env.clone();
540            if artifact.type_schema_init_chunk.is_some() || artifact.init_chunk.is_some() {
541                let saved_env = std::mem::replace(&mut self.env, init_env);
542                let saved_frames = std::mem::take(&mut self.frames);
543                let saved_handlers = std::mem::take(&mut self.exception_handlers);
544                let saved_iterators = std::mem::take(&mut self.iterators);
545                let saved_deadlines = std::mem::take(&mut self.deadlines);
546                // STEP_STACK / PERSONA_STACK are thread-locals shared with
547                // the calling frame. Emptying `self.frames` above means
548                // any `prune_below_frame(0)` triggered while the init
549                // chunk's bytecode runs — including the inevitable
550                // frame-pop prune at end-of-chunk — would wipe active
551                // steps owned by the *caller* (e.g., a `@step`-decorated
552                // function whose body lazily imports a module). Snapshot
553                // the persona/step context here and restore it after init
554                // so module loading is invisible to the step-tracking
555                // surface.
556                let active_context = crate::step_runtime::take_active_context();
557                let init_result: Result<(), VmError> = async {
558                    if let Some(chunk) = &artifact.type_schema_init_chunk {
559                        self.run_chunk(Arc::clone(chunk)).await?;
560                    }
561                    if let Some(chunk) = &artifact.init_chunk {
562                        self.run_chunk(Arc::clone(chunk)).await?;
563                    }
564                    Ok(())
565                }
566                .await;
567                crate::step_runtime::restore_active_context(active_context);
568                init_env = std::mem::replace(&mut self.env, saved_env);
569                self.frames = saved_frames;
570                self.exception_handlers = saved_handlers;
571                self.iterators = saved_iterators;
572                self.deadlines = saved_deadlines;
573                init_result?;
574            }
575            Arc::new(crate::value::VmMutex::new(init_env))
576        };
577
578        let module_env = self.env.clone();
579        let registry: ModuleFunctionRegistry =
580            Arc::new(crate::value::VmMutex::new(BTreeMap::new()));
581        let mut functions: BTreeMap<String, Arc<VmClosure>> = BTreeMap::new();
582        let mut public_exports = artifact.public_exports.clone();
583        // The init chunk already ran into `module_state`, so init-backed public
584        // values are live there. Read only the names identified by the artifact
585        // contract and publish their evaluated values for importers.
586        let mut public_values: BTreeMap<String, VmValue> = BTreeMap::new();
587        {
588            let state = module_state.lock();
589            for name in &artifact.public_value_names {
590                if let Some(value) = state.get(name) {
591                    public_values.insert(name.clone(), value);
592                }
593            }
594        }
595        let public_type_names = artifact.public_type_names.clone();
596        let mut public_type_schemas: BTreeMap<String, VmValue> = {
597            let state = module_state.lock();
598            public_type_names
599                .iter()
600                .filter_map(|name| state.get(name).map(|schema| (name.clone(), schema)))
601                .collect()
602        };
603
604        for (name, compiled) in &artifact.functions {
605            let closure = Arc::new(VmClosure {
606                func: Arc::clone(compiled),
607                env: module_env.clone(),
608                source_dir: module_source_dir.clone(),
609                module_functions: Some(Arc::downgrade(&registry)),
610                module_state: Some(Arc::downgrade(&module_state)),
611                retained_module_scope: None,
612            });
613            registry.lock().insert(name.clone(), Arc::clone(&closure));
614            self.env
615                .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
616            module_state
617                .lock()
618                .define(name, VmValue::Closure(Arc::clone(&closure)), false)?;
619            functions.insert(name.clone(), Arc::clone(&closure));
620        }
621
622        for import in artifact.imports.iter().filter(|import| import.is_pub) {
623            let cache_key = self.cache_key_for_import(&import.path)?;
624            let Some(loaded) = self.module_cache.get(&cache_key).cloned() else {
625                // A plain `import`/`import {...}` across a cycle is bound late
626                // by `flush_deferred_cyclic_imports`, but a `pub import`
627                // re-export has to publish the names into *this* module's
628                // public surface right now — and the target is still mid-load,
629                // so its surface does not exist yet. Name the cycle explicitly
630                // instead of the misleading "was not loaded".
631                if self.imported_paths.contains(&cache_key) {
632                    return Err(VmError::Runtime(format!(
633                        "Re-export error: cannot `pub import` from '{}' because it forms an \
634                         import cycle with this module (its public surface is still being \
635                         built). Use a plain `import` here, or re-export from a module that is \
636                         not part of the cycle.",
637                        import.path
638                    )));
639                }
640                return Err(VmError::Runtime(format!(
641                    "Re-export error: imported module '{}' was not loaded",
642                    import.path
643                )));
644            };
645            // `pub import * as alias` publishes the alias namespace object —
646            // never flatten target members into this module's public surface.
647            if let Some(alias) = &import.namespace_alias {
648                if public_exports.contains_key(alias) || functions.contains_key(alias) {
649                    return Err(VmError::Runtime(format!(
650                        "Re-export collision: '{alias}' is defined here and also \
651                         re-exported as a namespace from '{}'",
652                        import.path
653                    )));
654                }
655                let dict = build_namespace_dict(&import.path, &loaded);
656                public_values.insert(alias.clone(), dict);
657                public_exports.insert(alias.clone(), DefKind::Variable);
658                continue;
659            }
660            let names_to_reexport =
661                module_import_names(&import.path, &loaded, import.selected_names.as_deref())?;
662            for name in names_to_reexport {
663                let Some(kind) = loaded.public_exports.get(&name).copied() else {
664                    return Err(VmError::Runtime(format!(
665                        "Re-export error: '{name}' is not exported by '{}'",
666                        import.path
667                    )));
668                };
669                let Some(closure) = loaded.functions.get(&name) else {
670                    // Init-backed declarations carry their evaluated value
671                    // directly, including struct constructors and enum
672                    // namespaces.
673                    if let Some(value) = loaded.public_values.get(&name) {
674                        public_values.insert(name.clone(), value.clone());
675                        public_exports.insert(name, kind);
676                        continue;
677                    }
678                    // Type-only declarations carry no runtime binding. Preserve
679                    // an optional schema lowering and the contract entry.
680                    if let Some(schema) = loaded.public_type_schemas.get(&name) {
681                        public_type_schemas.insert(name.clone(), schema.clone());
682                    }
683                    public_exports.insert(name, kind);
684                    continue;
685                };
686                if let Some(existing) = functions.get(&name) {
687                    if !Arc::ptr_eq(existing, closure) {
688                        return Err(VmError::Runtime(format!(
689                            "Re-export collision: '{name}' is defined here and also \
690                             re-exported from '{}'",
691                            import.path
692                        )));
693                    }
694                }
695                functions.insert(name.clone(), Arc::clone(closure));
696                public_exports.insert(name, kind);
697            }
698        }
699
700        self.env = caller_env;
701        self.source_dir = old_source_dir;
702
703        Ok(LoadedModule {
704            functions,
705            public_exports,
706            public_values,
707            public_type_schemas,
708            package_execution_guard: module_source_dir
709                .as_ref()
710                .and(self.package_execution_guard.clone()),
711            _module_functions: registry,
712            _module_state: module_state,
713        })
714    }
715
716    fn export_namespace_module(
717        &mut self,
718        module_path: &Path,
719        loaded: &LoadedModule,
720        alias: &str,
721    ) -> Result<(), VmError> {
722        let module_name = module_path.display().to_string();
723        if self.env.get(alias).is_some() {
724            return Err(VmError::Runtime(format!(
725                "Import collision: '{alias}' is already defined when importing {module_name}. \
726                 Use a different namespace alias: import * as <name> from \"...\""
727            )));
728        }
729        let dict = build_namespace_dict(&module_name, loaded);
730        self.env.define(alias, dict, false)?;
731        Ok(())
732    }
733
734    fn export_loaded_module(
735        &mut self,
736        module_path: &Path,
737        loaded: &LoadedModule,
738        selected_names: Option<&[String]>,
739    ) -> Result<(), VmError> {
740        let module_name = module_path.display().to_string();
741        let export_names = module_import_names(&module_name, loaded, selected_names)?;
742
743        for name in export_names {
744            // `pub const` / `pub let` values: bind by value.
745            if let Some(value) = loaded.public_values.get(&name) {
746                if self.env.get(&name).is_some() {
747                    return Err(VmError::Runtime(format!(
748                        "Import collision: '{name}' is already defined when importing \
749                         {module_name}. Use selective imports to disambiguate: \
750                         import {{ {name} }} from \"...\""
751                    )));
752                }
753                self.env.define(&name, value.clone(), false)?;
754                continue;
755            }
756            // Type and interface declarations are valid imports without a
757            // runtime value. Schema-capable aliases still bind their schema so
758            // expression-position uses match local alias lowering.
759            if let Some(schema) = loaded.public_type_schemas.get(&name) {
760                self.env.define(&name, schema.clone(), false)?;
761                continue;
762            }
763            if loaded
764                .public_exports
765                .get(&name)
766                .is_some_and(|kind| !kind.has_runtime_value())
767            {
768                continue;
769            }
770            let Some(closure) = loaded.functions.get(&name) else {
771                return Err(VmError::Runtime(format!(
772                    "Import error: '{name}' is not defined in {module_name}"
773                )));
774            };
775            if let Some(VmValue::Closure(_)) = self.env.get(&name) {
776                return Err(VmError::Runtime(format!(
777                    "Import collision: '{name}' is already defined when importing {module_name}. \
778                     Use selective imports to disambiguate: import {{ {name} }} from \"...\""
779                )));
780            }
781            self.env
782                .define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
783        }
784        Ok(())
785    }
786
787    /// Execute an import, reading and running the file's declarations.
788    pub(super) fn execute_import<'a>(
789        &'a mut self,
790        path: &'a str,
791        selected_names: Option<&'a [String]>,
792    ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
793        self.execute_import_with_projection(path, ImportProjection::BindCaller(selected_names))
794    }
795
796    /// Bind `import * as alias from path` as a closed namespace dict.
797    pub(super) fn execute_namespace_import_bind<'a>(
798        &'a mut self,
799        path: &'a str,
800        alias: &'a str,
801    ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
802        self.execute_import_with_projection(path, ImportProjection::BindNamespace(alias))
803    }
804
805    fn materialize_import<'a>(
806        &'a mut self,
807        path: &'a str,
808    ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
809        self.execute_import_with_projection(path, ImportProjection::MaterializeOnly)
810    }
811
812    fn apply_import_projection(
813        &mut self,
814        module_path: &Path,
815        loaded: &LoadedModule,
816        projection: ImportProjection<'_>,
817    ) -> Result<(), VmError> {
818        match projection {
819            ImportProjection::BindCaller(selected_names) => {
820                self.export_loaded_module(module_path, loaded, selected_names)
821            }
822            ImportProjection::BindNamespace(alias) => {
823                self.export_namespace_module(module_path, loaded, alias)
824            }
825            ImportProjection::MaterializeOnly => Ok(()),
826        }
827    }
828
829    fn execute_import_with_projection<'a>(
830        &'a mut self,
831        path: &'a str,
832        projection: ImportProjection<'a>,
833    ) -> Pin<Box<dyn Future<Output = Result<(), VmError>> + Send + 'a>> {
834        Box::pin(async move {
835            let _import_span = ScopeSpan::new(crate::tracing::SpanKind::Import, path.to_string());
836
837            let stdlib_module = path
838                .strip_prefix("std/")
839                .or_else(|| (path == "observability").then_some("observability"));
840            if let Some(module) = stdlib_module {
841                if let Some(source) = crate::stdlib_modules::get_stdlib_source(module) {
842                    let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
843                    if self.imported_paths.contains(&synthetic) {
844                        return Ok(());
845                    }
846                    if let Some(loaded) = self.module_cache.get(&synthetic).cloned() {
847                        return self.apply_import_projection(&synthetic, &loaded, projection);
848                    }
849                    let loaded = self
850                        .load_stdlib_module_from_source(module, synthetic.clone(), source)
851                        .await?;
852                    if !matches!(projection, ImportProjection::MaterializeOnly) {
853                        let _load_span = self.module_load_span();
854                        self.apply_import_projection(&synthetic, &loaded, projection)?;
855                    }
856                    return Ok(());
857                }
858                return Err(VmError::Runtime(format!(
859                    "Unknown stdlib module: std/{module}"
860                )));
861            }
862
863            let base = self
864                .source_dir
865                .clone()
866                .unwrap_or_else(|| PathBuf::from("."));
867            let file_path = self.resolve_module_import_path(&base, path)?;
868            let verified_source = if let Some(guard) = &self.package_execution_guard {
869                let bytes = guard.verify_entry_source(&file_path).map_err(|error| {
870                    VmError::Runtime(format!(
871                        "installed package {} rejected: {error}",
872                        projection.package_rejection_kind()
873                    ))
874                })?;
875                Some(verified_package_source(bytes, &file_path)?)
876            } else {
877                None
878            };
879
880            let canonical = file_path
881                .canonicalize()
882                .unwrap_or_else(|_| file_path.clone());
883            if self.imported_paths.contains(&canonical) {
884                // Import cycle: `canonical` is still mid-load (it sits on the
885                // import stack), so its function closures don't exist yet and
886                // we cannot bind the requested names inline. Record the import
887                // and resolve it once both modules finish loading — otherwise
888                // whichever module happens to close the cycle silently loses
889                // these bindings and fails with `Undefined builtin` at call
890                // time, in a load-order-dependent way.
891                match projection {
892                    ImportProjection::BindCaller(selected_names) => {
893                        if let Some(importer) = self.imported_paths.last().cloned() {
894                            if importer != canonical {
895                                self.deferred_cyclic_imports.push(DeferredCyclicImport {
896                                    importer,
897                                    target: canonical.clone(),
898                                    selected_names: selected_names.map(<[String]>::to_vec),
899                                    namespace_alias: None,
900                                });
901                            }
902                        }
903                    }
904                    ImportProjection::BindNamespace(alias) => {
905                        if let Some(importer) = self.imported_paths.last().cloned() {
906                            if importer != canonical {
907                                self.deferred_cyclic_imports.push(DeferredCyclicImport {
908                                    importer,
909                                    target: canonical.clone(),
910                                    selected_names: None,
911                                    namespace_alias: Some(alias.to_string()),
912                                });
913                            }
914                        }
915                    }
916                    ImportProjection::MaterializeOnly => {}
917                }
918                return Ok(());
919            }
920            if let Some(loaded) = self.module_cache.get(&canonical).cloned() {
921                if let Some(source) = &verified_source {
922                    let cached_source = self.source_cache.get(&canonical);
923                    if cached_source != Some(source) {
924                        return Err(VmError::Runtime(format!(
925                            "installed package {} rejected: cached module {} was not compiled from the verified package bytes",
926                            projection.package_rejection_kind(),
927                            canonical.display()
928                        )));
929                    }
930                    let active_guard = self
931                        .package_execution_guard
932                        .as_deref()
933                        .expect("verified package source requires an active guard");
934                    if loaded.package_execution_guard.as_deref() != Some(active_guard) {
935                        return Err(VmError::Runtime(format!(
936                            "installed package {} rejected: cached module {} was not instantiated under the active package execution guard",
937                            projection.package_rejection_kind(),
938                            canonical.display()
939                        )));
940                    }
941                }
942                return self.apply_import_projection(&canonical, &loaded, projection);
943            }
944            self.imported_paths.push(canonical.clone());
945
946            let source = {
947                let _load_span = self.module_load_span();
948                match verified_source {
949                    Some(source) => source,
950                    None => std::fs::read_to_string(&file_path).map_err(|e| {
951                        // Name the resolution base: relative imports resolve against the
952                        // importing file's dir (or CWD when unset), so an error that
953                        // prints only the joined path leaves the author guessing which
954                        // base was used.
955                        VmError::Runtime(format!(
956                            "Import error: cannot read '{}' (resolved '{path}' relative to {}): {e}",
957                            file_path.display(),
958                            base.display()
959                        ))
960                    })?,
961                }
962            };
963            Arc::make_mut(&mut self.source_cache).insert(canonical.clone(), source.clone());
964            Arc::make_mut(&mut self.source_cache).insert(file_path.clone(), source.clone());
965
966            let prepared = {
967                let _load_span = self.module_load_span();
968                if bytecode_cache::cache_enabled() {
969                    self.prepared_module_cache.get(&canonical, &source)
970                } else {
971                    None
972                }
973            };
974            let artifact = if let Some(prepared) = prepared {
975                prepared
976            } else {
977                // Disk cache hits skip parse + compile. The scoped prepared
978                // cache additionally skips deserialization and chunk hydration
979                // on later fresh VMs without sharing any runtime module state.
980                let lookup = {
981                    let _load_span = self.module_load_span();
982                    bytecode_cache::load_module(&file_path, &source)
983                };
984                let cached = if let Some(artifact) = lookup.artifact {
985                    artifact
986                } else {
987                    let mut compile_span = self.module_compile_span();
988                    let compiled = compile_module_artifact_from_source(&file_path, &source)?;
989                    if let Some(span) = &mut compile_span {
990                        span.mark_compile_succeeded();
991                    }
992                    drop(compile_span);
993                    if let Err(err) = bytecode_cache::store_module(&lookup.key, &compiled) {
994                        if std::env::var_os("HARN_BYTECODE_CACHE_DEBUG").is_some() {
995                            eprintln!(
996                                "[harn] module cache write skipped for {}: {err}",
997                                file_path.display()
998                            );
999                        }
1000                    }
1001                    compiled
1002                };
1003                let mut prepared = {
1004                    let _load_span = self.module_load_span();
1005                    Arc::new(PreparedModuleArtifact::from_cached(cached))
1006                };
1007                if bytecode_cache::cache_enabled() {
1008                    prepared =
1009                        self.prepared_module_cache
1010                            .insert(canonical.clone(), &source, prepared);
1011                }
1012                prepared
1013            };
1014
1015            let module_source_dir = file_path.parent().map(|p| p.to_path_buf());
1016            let loaded = Arc::new(
1017                self.instantiate_module(module_source_dir, artifact.as_ref())
1018                    .await?,
1019            );
1020            self.imported_paths.pop();
1021            {
1022                let _load_span = self.module_load_span();
1023                Arc::make_mut(&mut self.module_cache)
1024                    .insert(canonical.clone(), Arc::clone(&loaded));
1025            }
1026            self.record_module_loaded();
1027            if !matches!(projection, ImportProjection::MaterializeOnly) {
1028                let _load_span = self.module_load_span();
1029                self.apply_import_projection(&canonical, &loaded, projection)?;
1030            }
1031
1032            // Once the import stack fully unwinds, every module reachable from
1033            // this top-level import is cached, so any deferred cyclic imports
1034            // can now bind against fully-loaded modules.
1035            if self.imported_paths.is_empty() {
1036                let _load_span = self.module_load_span();
1037                self.flush_deferred_cyclic_imports()?;
1038            }
1039
1040            Ok(())
1041        })
1042    }
1043
1044    /// Bind imports that were deferred because their target module was still
1045    /// mid-load (an import cycle). By the time the import stack has unwound,
1046    /// both the importing and target modules are fully instantiated and cached,
1047    /// so we can resolve the requested names against the target and define them
1048    /// into the importer's shared, mutable `module_state`. That env is the one
1049    /// every closure from the importing module consults (after its local env)
1050    /// at call time, so the late binding becomes visible without needing to
1051    /// rewrite the closures' captured lexical snapshots.
1052    fn flush_deferred_cyclic_imports(&mut self) -> Result<(), VmError> {
1053        if self.deferred_cyclic_imports.is_empty() {
1054            return Ok(());
1055        }
1056        let deferred = std::mem::take(&mut self.deferred_cyclic_imports);
1057        let mut still_pending = Vec::new();
1058        for import in deferred {
1059            let (Some(importer), Some(target)) = (
1060                self.module_cache.get(&import.importer).cloned(),
1061                self.module_cache.get(&import.target).cloned(),
1062            ) else {
1063                // One endpoint is not cached yet (a lazy import inside a
1064                // function body can defer before the other side loads). Keep
1065                // it for a later flush.
1066                still_pending.push(import);
1067                continue;
1068            };
1069
1070            let mut module_state = importer._module_state.lock();
1071            if let Some(alias) = &import.namespace_alias {
1072                if module_state.get(alias).is_none() {
1073                    let dict = build_namespace_dict(&import.target.display().to_string(), &target);
1074                    module_state.define(alias, dict, false)?;
1075                }
1076                continue;
1077            }
1078
1079            let export_names = module_import_names(
1080                &import.target.display().to_string(),
1081                &target,
1082                import.selected_names.as_deref(),
1083            )?;
1084
1085            for name in export_names {
1086                // A real local declaration (or an already-bound non-cyclic
1087                // import) wins over the cyclic re-binding.
1088                if module_state.get(&name).is_some() {
1089                    continue;
1090                }
1091                if let Some(closure) = target.functions.get(&name) {
1092                    module_state.define(&name, VmValue::Closure(Arc::clone(closure)), false)?;
1093                } else if let Some(value) = target.public_values.get(&name) {
1094                    // Init-backed public declarations imported across a cycle.
1095                    module_state.define(&name, value.clone(), false)?;
1096                } else if target
1097                    .public_exports
1098                    .get(&name)
1099                    .is_some_and(|kind| !kind.has_runtime_value())
1100                {
1101                    // Type-only public declarations carry no runtime binding.
1102                    continue;
1103                } else {
1104                    return Err(VmError::Runtime(format!(
1105                        "Import error: '{name}' is not defined in {}",
1106                        import.target.display()
1107                    )));
1108                }
1109            }
1110        }
1111        self.deferred_cyclic_imports = still_pending;
1112        Ok(())
1113    }
1114
1115    /// Return the path key that `execute_import` would use to cache the
1116    /// LoadedModule for this import string. Used by the re-export pass to
1117    /// look up the already-loaded source module after `execute_import`
1118    /// has populated [`Vm::module_cache`].
1119    fn cache_key_for_import(&self, path: &str) -> Result<PathBuf, VmError> {
1120        if let Some(module) = path
1121            .strip_prefix("std/")
1122            .or_else(|| (path == "observability").then_some("observability"))
1123        {
1124            return Ok(PathBuf::from(format!("<stdlib>/{module}.harn")));
1125        }
1126        let base = self
1127            .source_dir
1128            .clone()
1129            .unwrap_or_else(|| PathBuf::from("."));
1130        let file_path = self.resolve_module_import_path(&base, path)?;
1131        Ok(file_path.canonicalize().unwrap_or(file_path))
1132    }
1133
1134    async fn loaded_module_for_path(
1135        &mut self,
1136        path: &Path,
1137    ) -> Result<(PathBuf, Arc<LoadedModule>), VmError> {
1138        self.ensure_execution_available()?;
1139        let path_str = path.to_string_lossy().into_owned();
1140        self.materialize_import(&path_str).await?;
1141
1142        let mut file_path = if path.is_absolute() {
1143            path.to_path_buf()
1144        } else {
1145            self.source_dir
1146                .clone()
1147                .unwrap_or_else(|| PathBuf::from("."))
1148                .join(path)
1149        };
1150        if !file_path.exists() && file_path.extension().is_none() {
1151            file_path.set_extension("harn");
1152        }
1153
1154        let canonical = file_path
1155            .canonicalize()
1156            .unwrap_or_else(|_| file_path.clone());
1157        let loaded = self.module_cache.get(&canonical).cloned().ok_or_else(|| {
1158            VmError::Runtime(format!(
1159                "Import error: failed to cache loaded module '{}'",
1160                canonical.display()
1161            ))
1162        })?;
1163        Ok((canonical, loaded))
1164    }
1165
1166    /// Load one explicitly public callable from a module.
1167    pub async fn load_public_module_callable(
1168        &mut self,
1169        path: &Path,
1170        name: &str,
1171    ) -> Result<Arc<VmClosure>, VmError> {
1172        let (canonical, loaded) = self.loaded_module_for_path(path).await?;
1173        if !loaded.public_exports.contains_key(name) {
1174            let hint = if loaded.functions.contains_key(name) {
1175                "; it is defined there but not `pub`"
1176            } else {
1177                ""
1178            };
1179            return Err(VmError::Runtime(format!(
1180                "callable '{name}' is not exported by module '{}'{hint}",
1181                canonical.display()
1182            )));
1183        }
1184        loaded.functions.get(name).cloned().ok_or_else(|| {
1185            VmError::Runtime(format!(
1186                "Import error: exported callable '{name}' is missing from {}",
1187                canonical.display()
1188            ))
1189        })
1190    }
1191
1192    /// Load a module file and return the exported function closures that
1193    /// would be visible to a wildcard import.
1194    pub async fn load_module_exports(
1195        &mut self,
1196        path: &Path,
1197    ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
1198        let (canonical, loaded) = self.loaded_module_for_path(path).await?;
1199        exported_function_closures(&loaded, &canonical)
1200    }
1201
1202    /// Load synthetic source keyed by a synthetic module path and return
1203    /// the exported function closures that a wildcard import would expose.
1204    pub async fn load_module_exports_from_source(
1205        &mut self,
1206        source_key: impl Into<PathBuf>,
1207        source: &str,
1208    ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
1209        self.ensure_execution_available()?;
1210        let synthetic = source_key.into();
1211        let loaded = self
1212            .load_module_from_source(synthetic.clone(), source)
1213            .await?;
1214        exported_function_closures(&loaded, &synthetic)
1215    }
1216
1217    /// Load one callable from synthetic source for a host dispatch surface
1218    /// that has already selected the callable through its own policy. This is
1219    /// deliberately separate from module exports: script imports must
1220    /// continue to see only declarations in the typed public export contract.
1221    pub async fn load_module_callable_from_source(
1222        &mut self,
1223        source_key: impl Into<PathBuf>,
1224        source: &str,
1225        name: &str,
1226    ) -> Result<Option<Arc<VmClosure>>, VmError> {
1227        self.ensure_execution_available()?;
1228        let synthetic = source_key.into();
1229        let loaded = self.load_module_from_source(synthetic, source).await?;
1230        Ok(loaded.functions.get(name).cloned())
1231    }
1232
1233    /// Load a module by import path (`std/foo`, relative module path, or
1234    /// package import) and return the exported function closures that a
1235    /// wildcard import would expose.
1236    pub async fn load_module_exports_from_import(
1237        &mut self,
1238        import_path: &str,
1239    ) -> Result<BTreeMap<String, Arc<VmClosure>>, VmError> {
1240        self.ensure_execution_available()?;
1241        self.materialize_import(import_path).await?;
1242
1243        if let Some(module) = import_path
1244            .strip_prefix("std/")
1245            .or_else(|| (import_path == "observability").then_some("observability"))
1246        {
1247            let synthetic = PathBuf::from(format!("<stdlib>/{module}.harn"));
1248            let loaded = self.module_cache.get(&synthetic).cloned().ok_or_else(|| {
1249                VmError::Runtime(format!(
1250                    "Import error: failed to cache loaded module '{}'",
1251                    synthetic.display()
1252                ))
1253            })?;
1254            return exported_function_closures(&loaded, &synthetic);
1255        }
1256
1257        let base = self
1258            .source_dir
1259            .clone()
1260            .unwrap_or_else(|| PathBuf::from("."));
1261        let file_path = self.resolve_module_import_path(&base, import_path)?;
1262        self.load_module_exports(&file_path).await
1263    }
1264}
1265
1266#[cfg(test)]
1267#[path = "modules_tests.rs"]
1268mod tests;