Skip to main content

harn_vm/vm/
modules.rs

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