Skip to main content

harn_vm/
module_artifact.rs

1//! Serializable shape of a compiled `.harn` module — the unit the
2//! on-disk module cache stores.
3//!
4//! A module is anything `import` can name: a stdlib file (`std/foo`) or
5//! a user file on disk. The artifact captures **only** the result of
6//! the parse + compile pipeline; instantiation (running the `init`
7//! chunk, creating closures bound to a fresh module env, and applying
8//! re-exports) happens fresh per process and is not cached. This split
9//! lets the cache short-circuit the expensive parse+compile while still
10//! producing the per-process state the runtime needs.
11
12use std::collections::{BTreeMap, HashSet};
13use std::path::{Path, PathBuf};
14use std::sync::{Mutex, OnceLock};
15
16use harn_modules::{public_declarations, DefKind};
17use serde::{Deserialize, Serialize};
18use sha2::{Digest, Sha256};
19
20use crate::chunk::{CachedChunk, CachedCompiledFunction};
21use crate::value::VmError;
22
23/// The complete imported interface that can affect one module's bytecode.
24///
25/// Dependency bodies remain independently cached. Only the canonical names and
26/// kinds consulted during lowering belong here, so every in-memory, on-disk,
27/// precompiled, and warm-link identity can share this one typed owner.
28#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub struct ModuleCompilationContext {
30    enum_candidates: Vec<String>,
31    source_callable_names: Vec<String>,
32}
33
34impl ModuleCompilationContext {
35    pub fn new(
36        enum_candidates: impl IntoIterator<Item = String>,
37        source_callable_names: impl IntoIterator<Item = String>,
38    ) -> Self {
39        let mut enum_candidates = enum_candidates.into_iter().collect::<Vec<_>>();
40        enum_candidates.sort_unstable();
41        enum_candidates.dedup();
42        let mut source_callable_names = source_callable_names.into_iter().collect::<Vec<_>>();
43        source_callable_names.sort_unstable();
44        source_callable_names.dedup();
45        Self {
46            enum_candidates,
47            source_callable_names,
48        }
49    }
50
51    pub fn from_graph(graph: &harn_modules::ModuleGraph, source_path: &Path) -> Self {
52        Self::new(
53            graph
54                .imported_names_by_kind_for_file(source_path, DefKind::Enum)
55                .unwrap_or_default(),
56            graph
57                .imported_callable_names_for_file(source_path)
58                .unwrap_or_default(),
59        )
60    }
61
62    /// Project only names that this source's syntax can consult during
63    /// lowering. This keeps eager graph prewarming and lazy compilation on the
64    /// same canonical identity without forcing graph work for insensitive
65    /// modules.
66    pub fn for_source_in_graph(
67        graph: &harn_modules::ModuleGraph,
68        source_path: &Path,
69        source: &str,
70    ) -> Result<Self, VmError> {
71        let program = parse_module_source(source_path, source)?;
72        Ok(if needs_imported_symbol_projection(&program) {
73            Self::from_graph(graph, source_path)
74        } else {
75            Self::default()
76        })
77    }
78
79    pub fn enum_candidates(&self) -> &[String] {
80        &self.enum_candidates
81    }
82
83    pub fn source_callable_names(&self) -> &[String] {
84        &self.source_callable_names
85    }
86
87    /// Stable, framed identity for cache keys. The domain tag makes future
88    /// context families non-aliasing without coupling callers to this layout.
89    pub fn digest(&self) -> [u8; 32] {
90        let mut hasher = Sha256::new();
91        hasher.update(b"harn-module-compilation-context-v1\0");
92        hash_names(&mut hasher, b"enum\0", &self.enum_candidates);
93        hash_names(
94            &mut hasher,
95            b"source-callable\0",
96            &self.source_callable_names,
97        );
98        hasher.finalize().into()
99    }
100}
101
102fn hash_names(hasher: &mut Sha256, label: &[u8], names: &[String]) {
103    hasher.update(label);
104    hasher.update((names.len() as u64).to_le_bytes());
105    for name in names {
106        hasher.update((name.len() as u64).to_le_bytes());
107        hasher.update(name.as_bytes());
108    }
109}
110
111type ImportedSymbolCache = BTreeMap<PathBuf, ([u8; 32], ModuleCompilationContext)>;
112
113/// Authority provenance carried by a compiled module.
114///
115/// Ordinary source compilation always produces [`User`](Self::User).
116/// Privileged variants can only be selected through explicit trusted-embedder
117/// entry points; there is no source annotation, filename convention, or
118/// environment switch that grants them.
119#[derive(
120    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
121)]
122pub enum ModuleProvenance {
123    #[default]
124    User,
125    /// Immutable source selected from the runtime-owned stdlib catalog.
126    /// There is no source spelling for this authority: only the embedded
127    /// module loader and closed-program linker can construct it.
128    EmbeddedStdlib,
129    PrivilegedWire,
130    /// A Rust embedder-selected route module and its private import graph.
131    /// Unlike `PrivilegedWire`, callables may be exported because only the
132    /// selecting host can receive them; ordinary Harn imports never load this
133    /// provenance.
134    TrustedHostDispatch,
135}
136
137fn imported_symbol_cache() -> &'static Mutex<ImportedSymbolCache> {
138    static CACHE: OnceLock<Mutex<ImportedSymbolCache>> = OnceLock::new();
139    CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
140}
141
142/// A single `import`-style declaration inside a module. Re-resolved at
143/// instantiation time so that the cached artifact does not bake in
144/// stale resolved paths.
145#[derive(Clone, Debug, Serialize, Deserialize)]
146pub struct ModuleImportSpec {
147    pub path: String,
148    pub binding: ModuleImportBinding,
149    pub is_pub: bool,
150}
151
152/// The mutually exclusive binding forms of an import declaration.
153#[derive(Clone, Debug, Serialize, Deserialize)]
154pub enum ModuleImportBinding {
155    Wildcard,
156    Selected(Vec<String>),
157    Namespace {
158        alias: String,
159        demand: harn_parser::NamespaceDemand,
160    },
161}
162
163/// Serializable compile artifact for one `.harn` module. The runtime
164/// turns this into a loaded module by replaying [`init_chunk`](Self::init_chunk)
165/// into a fresh env, minting closures for each entry in
166/// [`functions`](Self::functions), and re-issuing every nested
167/// [`imports`](Self::imports).
168#[derive(Clone, Debug, Serialize, Deserialize)]
169pub struct ModuleArtifact {
170    #[serde(default)]
171    pub provenance: ModuleProvenance,
172    pub imports: Vec<ModuleImportSpec>,
173    /// Cached bytecode that materializes exported type aliases after imports
174    /// are bound and before value initialization runs.
175    pub type_schema_init_chunks: Vec<CachedChunk>,
176    pub init_chunk: Option<CachedChunk>,
177    pub functions: BTreeMap<String, CachedCompiledFunction>,
178    /// The public declaration contract shared with `harn-modules`. Each name
179    /// carries its source declaration kind so the loader can choose a closure,
180    /// initialized value, schema, or type-only projection without maintaining
181    /// a second AST export table.
182    pub public_exports: BTreeMap<String, DefKind>,
183    /// Public declarations whose runtime value is produced by replaying
184    /// [`init_chunk`](Self::init_chunk), rather than the precompiled function
185    /// table. This includes bindings, enums, tools, skills, and eval packs.
186    pub public_value_names: HashSet<String>,
187    /// Names of erased public type declarations (`type` and `interface`). They
188    /// carry no runtime value of their own, but importers may still name them
189    /// in selective imports. Public structs and enums are excluded because
190    /// they export runtime constructors/namespaces.
191    pub public_type_names: HashSet<String>,
192}
193
194/// Specialize a fully compiled module for one closed-program export demand.
195///
196/// Module initialization and every import spec remain intact. Only public
197/// projection metadata, exported type schema initialization, and callable
198/// bytecode proven unreachable from initialization or retained members are
199/// removed. Generic module caches never call this function.
200pub fn specialize_module_artifact(
201    program: &[harn_parser::SNode],
202    source_file: Option<String>,
203    mut artifact: ModuleArtifact,
204    demand: &harn_modules::ExportDemand,
205) -> Result<ModuleArtifact, VmError> {
206    use harn_parser::Node;
207    use std::collections::{BTreeSet, HashMap};
208
209    if matches!(demand, harn_modules::ExportDemand::WholeNamespace) {
210        return Ok(artifact);
211    }
212
213    // Public re-exports currently share one projection with local bindings.
214    // Until that runtime contract gains a distinct re-export projection,
215    // pruning such a module could either leak extra exports or remove names its
216    // own code uses. Widen locally rather than weakening semantics.
217    if artifact.imports.iter().any(|import| import.is_pub) {
218        return Ok(artifact);
219    }
220
221    let callable_names = artifact.functions.keys().cloned().collect::<HashSet<_>>();
222    let mut declarations = HashMap::<String, &harn_parser::SNode>::new();
223    for node in program {
224        let inner = match &node.node {
225            Node::AttributedDecl { inner, .. } => inner.as_ref(),
226            _ => node,
227        };
228        let name = match &inner.node {
229            Node::FnDecl { name, .. }
230            | Node::Pipeline { name, .. }
231            | Node::StructDecl { name, .. } => Some(name),
232            _ => None,
233        };
234        if let Some(name) = name {
235            declarations.insert(name.clone(), inner);
236        }
237    }
238
239    let mut pending = Vec::new();
240    if let harn_modules::ExportDemand::Members(members) = demand {
241        pending.extend(
242            members
243                .iter()
244                .filter(|name| callable_names.contains(*name))
245                .cloned(),
246        );
247    }
248    // Every initializer is preserved, so every callable it can reach is a root.
249    for node in program {
250        let inner = match &node.node {
251            Node::AttributedDecl { inner, .. } => inner.as_ref(),
252            _ => node,
253        };
254        if matches!(
255            &inner.node,
256            Node::LetBinding { .. }
257                | Node::ConstBinding { .. }
258                | Node::EnumDecl { is_pub: true, .. }
259                | Node::ToolDecl { .. }
260                | Node::SkillDecl { .. }
261                | Node::EvalPackDecl { .. }
262        ) {
263            collect_callable_references(inner, &callable_names, &mut pending);
264        }
265    }
266
267    let mut retained = HashSet::new();
268    while let Some(name) = pending.pop() {
269        if !retained.insert(name.clone()) {
270            continue;
271        }
272        if let Some(declaration) = declarations.get(&name) {
273            collect_callable_references(declaration, &callable_names, &mut pending);
274        }
275    }
276    artifact.functions.retain(|name, _| retained.contains(name));
277    artifact
278        .public_exports
279        .retain(|name, _| demand.contains(name));
280    artifact
281        .public_value_names
282        .retain(|name| demand.contains(name));
283    artifact
284        .public_type_names
285        .retain(|name| demand.contains(name));
286
287    let selected_type_names = artifact
288        .public_type_names
289        .iter()
290        .cloned()
291        .collect::<BTreeSet<_>>();
292    artifact.type_schema_init_chunks =
293        crate::Compiler::compile_selected_public_type_schema_initializers(
294            program,
295            source_file,
296            Some(&selected_type_names),
297        )
298        .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
299        .into_iter()
300        .map(|chunk| chunk.freeze_for_cache())
301        .collect();
302    Ok(artifact)
303}
304
305fn collect_callable_references(
306    node: &harn_parser::SNode,
307    callable_names: &HashSet<String>,
308    out: &mut Vec<String>,
309) {
310    use harn_parser::Node;
311    let referenced = match &node.node {
312        Node::Identifier(name)
313        | Node::FunctionCall { name, .. }
314        | Node::StructConstruct {
315            struct_name: name, ..
316        }
317        | Node::EnumConstruct {
318            enum_name: name, ..
319        } => Some(name),
320        _ => None,
321    };
322    if let Some(name) = referenced.filter(|name| callable_names.contains(*name)) {
323        out.push(name.clone());
324    }
325    for child in harn_parser::visit::immediate_children(node) {
326        collect_callable_references(child, callable_names, out);
327    }
328}
329
330impl ModuleArtifact {
331    /// Bind relocatable cached bytecode to the source path used by this load.
332    ///
333    /// Module artifacts may move beside their source (`harn precompile`) or
334    /// inside a package. Source paths are diagnostic/debug context, not a
335    /// compilation input, so deserialize once and stamp every nested chunk at
336    /// the load boundary instead of duplicating otherwise-identical artifacts.
337    pub(crate) fn bind_source_file(&mut self, source_path: &Path) {
338        let source_file = source_path.display().to_string();
339        for chunk in &mut self.type_schema_init_chunks {
340            bind_chunk_source_file(chunk, &source_file);
341        }
342        if let Some(chunk) = &mut self.init_chunk {
343            bind_chunk_source_file(chunk, &source_file);
344        }
345        for function in self.functions.values_mut() {
346            bind_chunk_source_file(&mut function.chunk, &source_file);
347        }
348    }
349}
350
351fn bind_chunk_source_file(chunk: &mut CachedChunk, source_file: &str) {
352    chunk.source_file = Some(source_file.to_string());
353    for function in &mut chunk.functions {
354        bind_chunk_source_file(&mut function.chunk, source_file);
355    }
356}
357
358/// Compile a parsed `.harn` module into the serializable artifact shape.
359/// Pure compilation — no I/O, no execution. Used by both the runtime
360/// import path (`crates/harn-vm/src/vm/modules.rs`) and the
361/// `harn precompile` CLI subcommand.
362pub fn compile_module_artifact(
363    program: &[harn_parser::SNode],
364    module_source_file: Option<String>,
365) -> Result<ModuleArtifact, VmError> {
366    let imported_symbols = module_source_file
367        .as_deref()
368        .filter(|_| needs_imported_symbol_projection(program))
369        .map(|path| {
370            let graph = harn_modules::build(&[Path::new(path).to_path_buf()]);
371            sorted_imported_symbol_projection(&graph, Path::new(path))
372        })
373        .unwrap_or_default();
374    compile_module_artifact_with_imported_symbols(
375        program,
376        module_source_file,
377        &imported_symbols.enum_candidates,
378        &imported_symbols.source_callable_names,
379    )
380}
381
382fn compile_module_artifact_with_imported_symbols(
383    program: &[harn_parser::SNode],
384    module_source_file: Option<String>,
385    imported_enum_candidates: &[String],
386    imported_source_callable_names: &[String],
387) -> Result<ModuleArtifact, VmError> {
388    compile_module_artifact_with_provenance(
389        program,
390        module_source_file,
391        imported_enum_candidates,
392        imported_source_callable_names,
393        ModuleProvenance::User,
394    )
395}
396
397fn compile_module_artifact_with_provenance(
398    program: &[harn_parser::SNode],
399    module_source_file: Option<String>,
400    imported_enum_candidates: &[String],
401    imported_source_callable_names: &[String],
402    provenance: ModuleProvenance,
403) -> Result<ModuleArtifact, VmError> {
404    let options = match provenance {
405        ModuleProvenance::User => crate::CompilerOptions::from_env(),
406        ModuleProvenance::EmbeddedStdlib => crate::CompilerOptions::embedded_stdlib(),
407        ModuleProvenance::PrivilegedWire | ModuleProvenance::TrustedHostDispatch => {
408            crate::CompilerOptions::privileged_wire()
409        }
410    };
411    compile_module_artifact_with_options(
412        program,
413        module_source_file,
414        imported_enum_candidates,
415        imported_source_callable_names,
416        provenance,
417        options,
418    )
419}
420
421fn compile_module_artifact_with_options(
422    program: &[harn_parser::SNode],
423    module_source_file: Option<String>,
424    imported_enum_candidates: &[String],
425    imported_source_callable_names: &[String],
426    provenance: ModuleProvenance,
427    options: crate::CompilerOptions,
428) -> Result<ModuleArtifact, VmError> {
429    let namespace_demands = harn_parser::namespace_import_demands(program);
430    let imports: Vec<ModuleImportSpec> = program
431        .iter()
432        .filter_map(|node| match &node.node {
433            harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
434                path: path.clone(),
435                binding: ModuleImportBinding::Wildcard,
436                is_pub: *is_pub,
437            }),
438            harn_parser::Node::SelectiveImport {
439                names,
440                path,
441                is_pub,
442            } => Some(ModuleImportSpec {
443                path: path.clone(),
444                binding: ModuleImportBinding::Selected(names.clone()),
445                is_pub: *is_pub,
446            }),
447            harn_parser::Node::NamespaceImport {
448                alias,
449                path,
450                is_pub,
451            } => Some(ModuleImportSpec {
452                path: path.clone(),
453                binding: ModuleImportBinding::Namespace {
454                    alias: alias.clone(),
455                    demand: namespace_demands
456                        .get(alias)
457                        .cloned()
458                        .unwrap_or(harn_parser::NamespaceDemand::Whole),
459                },
460                is_pub: *is_pub,
461            }),
462            _ => None,
463        })
464        .collect();
465
466    if provenance == ModuleProvenance::PrivilegedWire {
467        validate_privileged_wire_surface(program, &imports)?;
468    }
469
470    let compiler = || crate::Compiler::with_options(options);
471
472    let init_nodes: Vec<harn_parser::SNode> = program
473        .iter()
474        .filter(|sn| {
475            let inner = match &sn.node {
476                harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
477                _ => sn,
478            };
479            matches!(
480                &inner.node,
481                harn_parser::Node::LetBinding { .. }
482                    | harn_parser::Node::ConstBinding { .. }
483                    // Only public enums need a runtime namespace in an
484                    // imported module. Private enum construction lowers
485                    // directly to `BuildEnum`, just like local construction,
486                    // so materializing a private namespace only adds cold
487                    // module-init work and closures that can never be
488                    // imported.
489                    | harn_parser::Node::EnumDecl { is_pub: true, .. }
490                    | harn_parser::Node::ToolDecl { .. }
491                    | harn_parser::Node::SkillDecl { .. }
492                    | harn_parser::Node::EvalPackDecl { .. }
493            )
494        })
495        .cloned()
496        .collect();
497    let init_chunk = if init_nodes.is_empty() {
498        None
499    } else {
500        let compiler = compiler();
501        Some(
502            compiler
503                .compile_module_init(
504                    program,
505                    &init_nodes,
506                    imported_enum_candidates,
507                    imported_source_callable_names,
508                )
509                .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
510                .freeze_for_cache(),
511        )
512    };
513
514    let public_exports: BTreeMap<String, DefKind> = program
515        .iter()
516        .flat_map(public_declarations)
517        .map(|export| (export.name, export.kind))
518        .collect();
519    let public_value_names = public_exports
520        .iter()
521        .filter(|(_, kind)| {
522            matches!(
523                kind,
524                DefKind::Variable
525                    | DefKind::Enum
526                    | DefKind::Tool
527                    | DefKind::Skill
528                    | DefKind::EvalPack
529            )
530        })
531        .map(|(name, _)| name.clone())
532        .collect();
533    let public_type_names = public_exports
534        .iter()
535        .filter(|(_, kind)| !kind.has_runtime_value())
536        .map(|(name, _)| name.clone())
537        .collect();
538
539    let mut functions = BTreeMap::new();
540    for node in program {
541        let inner = match &node.node {
542            harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
543            _ => node,
544        };
545        if let harn_parser::Node::StructDecl { name, fields, .. } = &inner.node {
546            // Struct constructors are ordinary module callables. Keeping
547            // them in the artifact function table avoids replaying the
548            // declaration through the module-init chunk while preserving
549            // private struct use and public constructor imports.
550            let constructor = compiler()
551                .compile_struct_constructor(name, fields)
552                .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
553            functions.insert(name.clone(), constructor.freeze_for_cache());
554            continue;
555        }
556        if let harn_parser::Node::Pipeline {
557            name,
558            params,
559            body,
560            extends,
561            ..
562        } = &inner.node
563        {
564            let mut compiler = compiler();
565            compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
566            compiler
567                .add_imported_source_callable_names(imported_source_callable_names.iter().cloned());
568            let pipeline = compiler
569                .compile_pipeline_callable(program, name, params, body, extends.as_deref())
570                .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
571            functions.insert(name.clone(), pipeline.freeze_for_cache());
572            continue;
573        }
574        let harn_parser::Node::FnDecl {
575            name,
576            type_params,
577            params,
578            body,
579            ..
580        } = &inner.node
581        else {
582            continue;
583        };
584
585        let mut compiler = compiler();
586        compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
587        compiler.add_imported_source_callable_names(imported_source_callable_names.iter().cloned());
588        compiler.prepare_module_context(program);
589        let func_chunk = compiler
590            .compile_named_fn_body(name, type_params, params, body, module_source_file.clone())
591            .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
592        functions.insert(name.clone(), func_chunk.freeze_for_cache());
593    }
594
595    let type_schema_init_chunks =
596        crate::Compiler::compile_public_type_schema_initializers(program, module_source_file)
597            .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
598            .into_iter()
599            .map(|chunk| chunk.freeze_for_cache())
600            .collect();
601
602    Ok(ModuleArtifact {
603        provenance,
604        imports,
605        type_schema_init_chunks,
606        init_chunk,
607        functions,
608        public_exports,
609        public_value_names,
610        public_type_names,
611    })
612}
613
614fn validate_privileged_wire_surface(
615    program: &[harn_parser::SNode],
616    imports: &[ModuleImportSpec],
617) -> Result<(), VmError> {
618    if imports.iter().any(|import| import.is_pub) {
619        return Err(VmError::Runtime(
620            "Privileged wire modules cannot re-export imports".to_string(),
621        ));
622    }
623    for export in program.iter().flat_map(public_declarations) {
624        if export.kind.has_runtime_value() && export.kind != DefKind::Variable {
625            return Err(VmError::Runtime(format!(
626                "Privileged wire module export `{}` is a {:?}; only explicit capability-value bindings may cross the wire boundary",
627                export.name, export.kind
628            )));
629        }
630    }
631    Ok(())
632}
633
634/// Lex + parse + [`compile_module_artifact`] in one call. Used when the
635/// caller already has the raw source bytes and wants the artifact in one
636/// step.
637pub fn compile_module_artifact_from_source(
638    source_path: &Path,
639    source: &str,
640) -> Result<ModuleArtifact, VmError> {
641    let program = parse_module_source(source_path, source)?;
642    let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
643    compile_module_artifact_with_imported_symbols(
644        &program,
645        Some(source_path.display().to_string()),
646        &imported_symbols.enum_candidates,
647        &imported_symbols.source_callable_names,
648    )
649}
650
651/// Compile source selected from the runtime's embedded stdlib catalog.
652///
653/// The caller must already have resolved the source through that catalog;
654/// filesystem paths and source annotations cannot select this entry point.
655pub(crate) fn compile_embedded_stdlib_module_artifact_from_source(
656    source_path: &Path,
657    source: &str,
658) -> Result<ModuleArtifact, VmError> {
659    let program = parse_module_source(source_path, source)?;
660    let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
661    compile_module_artifact_with_provenance(
662        &program,
663        Some(source_path.display().to_string()),
664        &imported_symbols.enum_candidates,
665        &imported_symbols.source_callable_names,
666        ModuleProvenance::EmbeddedStdlib,
667    )
668}
669
670// Test seam: how many times an imported interface has been resolved from
671// source on this thread.
672//
673// Resolving one costs a full lex and parse of the module. A cache hit that
674// still pays for it produces exactly the same artifact as one that does not,
675// so no output distinguishes them; only the work differs, and this counts it.
676// Thread-local so tests running in parallel cannot perturb each other.
677#[cfg(test)]
678thread_local! {
679    pub(crate) static INTERFACE_RESOLUTIONS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
680}
681
682/// Resolve the exact imported interface consulted while lowering `source`.
683/// Callers that cache or precompile the resulting artifact must carry this
684/// value alongside the source identity rather than reconstructing it later.
685pub fn module_compilation_context_for_source(
686    source_path: &Path,
687    source: &str,
688) -> Result<ModuleCompilationContext, VmError> {
689    #[cfg(test)]
690    INTERFACE_RESOLUTIONS.with(|count| count.set(count.get() + 1));
691    let program = parse_module_source(source_path, source)?;
692    Ok(imported_symbol_projection_for_program(
693        source_path,
694        source,
695        &program,
696    ))
697}
698
699/// Compile a trusted embedder-owned module that may call
700/// [`BuiltinExposure::PrivilegedWire`](harn_builtin_meta::BuiltinExposure)
701/// primitives.
702///
703/// The resulting module may export only initialized capability values and
704/// erased types. Runtime instantiation validates those values again, so
705/// closures, dictionaries, and arbitrary host results cannot smuggle wire
706/// authority into user modules.
707pub fn compile_privileged_wire_module_artifact_from_source(
708    source_path: &Path,
709    source: &str,
710) -> Result<ModuleArtifact, VmError> {
711    let program = parse_module_source(source_path, source)?;
712    let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
713    compile_module_artifact_with_provenance(
714        &program,
715        Some(source_path.display().to_string()),
716        &imported_symbols.enum_candidates,
717        &imported_symbols.source_callable_names,
718        ModuleProvenance::PrivilegedWire,
719    )
720}
721
722/// Compile one module in a Rust embedder-owned host-dispatch graph.
723///
724/// The runtime loader is responsible for keeping this provenance outside the
725/// ordinary import/cache path and returning only the host-selected callable.
726pub fn compile_trusted_host_dispatch_module_artifact_from_source(
727    source_path: &Path,
728    source: &str,
729) -> Result<ModuleArtifact, VmError> {
730    let program = parse_module_source(source_path, source)?;
731    let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
732    compile_module_artifact_with_provenance(
733        &program,
734        Some(source_path.display().to_string()),
735        &imported_symbols.enum_candidates,
736        &imported_symbols.source_callable_names,
737        ModuleProvenance::TrustedHostDispatch,
738    )
739}
740
741/// Compile a trusted host-dispatch module with an already-resolved enum-import
742/// projection. Suite prewarming uses this entry point so it can share the
743/// module graph walk without weakening provenance or rebuilding the graph in
744/// every fresh VM.
745pub fn compile_trusted_host_dispatch_module_artifact_from_source_with_imported_enums(
746    source_path: &Path,
747    source: &str,
748    imported_enum_candidates: impl IntoIterator<Item = String>,
749) -> Result<ModuleArtifact, VmError> {
750    let program = parse_module_source(source_path, source)?;
751    let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
752    let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
753    compile_module_artifact_with_provenance(
754        &program,
755        Some(source_path.display().to_string()),
756        &imported_enum_candidates,
757        &imported_symbols.source_callable_names,
758        ModuleProvenance::TrustedHostDispatch,
759    )
760}
761
762pub fn compile_trusted_host_dispatch_module_artifact_from_source_with_imported_symbols(
763    source_path: &Path,
764    source: &str,
765    imported_enum_candidates: impl IntoIterator<Item = String>,
766    imported_source_callable_names: impl IntoIterator<Item = String>,
767) -> Result<ModuleArtifact, VmError> {
768    let context =
769        ModuleCompilationContext::new(imported_enum_candidates, imported_source_callable_names);
770    compile_trusted_host_dispatch_module_artifact_from_source_with_context(
771        source_path,
772        source,
773        &context,
774    )
775}
776
777pub fn compile_trusted_host_dispatch_module_artifact_from_source_with_context(
778    source_path: &Path,
779    source: &str,
780    context: &ModuleCompilationContext,
781) -> Result<ModuleArtifact, VmError> {
782    let program = parse_module_source(source_path, source)?;
783    compile_module_artifact_with_provenance(
784        &program,
785        Some(source_path.display().to_string()),
786        context.enum_candidates(),
787        context.source_callable_names(),
788        ModuleProvenance::TrustedHostDispatch,
789    )
790}
791
792/// Resolve syntax-sensitive imported symbols in one module-graph walk.
793///
794/// Enum-pattern lowering and wildcard callable shadowing need different
795/// projections of the same graph. Keeping them together avoids rebuilding a
796/// large stdlib closure once per compiler catalog.
797fn imported_symbol_projection_for_program(
798    source_path: &Path,
799    source: &str,
800    program: &[harn_parser::SNode],
801) -> ModuleCompilationContext {
802    if !needs_imported_symbol_projection(program) {
803        return ModuleCompilationContext::default();
804    }
805    let source_hash = *blake3::hash(source.as_bytes()).as_bytes();
806    let cache_key = harn_modules::canonical_path(source_path);
807    let cacheable = is_immutable_stdlib_path(source_path);
808    if cacheable {
809        if let Some((_cached_hash, projection)) = imported_symbol_cache()
810            .lock()
811            .expect("imported symbol cache lock poisoned")
812            .get(&cache_key)
813            .filter(|(cached_hash, _)| *cached_hash == source_hash)
814        {
815            return projection.clone();
816        }
817    }
818
819    // The result describes every module in the closure. Publish all those
820    // projections at once so loading a large stdlib does not rebuild it once
821    // per module artifact or once per compiler catalog.
822    let graph = harn_modules::build_with_source(source_path, source);
823    if !cacheable {
824        return sorted_imported_symbol_projection(&graph, source_path);
825    }
826    let mut projections = Vec::new();
827    for path in graph.module_paths() {
828        let module_source = if path == cache_key {
829            Some(source.to_string())
830        } else {
831            harn_modules::read_module_source(&path).or_else(|| std::fs::read_to_string(&path).ok())
832        };
833        let Some(module_source) = module_source else {
834            continue;
835        };
836        let projection = sorted_imported_symbol_projection(&graph, &path);
837        projections.push((
838            path,
839            (
840                *blake3::hash(module_source.as_bytes()).as_bytes(),
841                projection,
842            ),
843        ));
844    }
845    let mut cache = imported_symbol_cache()
846        .lock()
847        .expect("imported symbol cache lock poisoned");
848    for (path, projection) in projections {
849        if is_immutable_stdlib_path(&path) {
850            cache.insert(path, projection);
851        }
852    }
853    cache
854        .get(&cache_key)
855        .filter(|(cached_hash, _)| *cached_hash == source_hash)
856        .map(|(_, projection)| projection.clone())
857        .unwrap_or_default()
858}
859
860fn sorted_imported_symbol_projection(
861    graph: &harn_modules::ModuleGraph,
862    source_path: &Path,
863) -> ModuleCompilationContext {
864    ModuleCompilationContext::from_graph(graph, source_path)
865}
866
867fn is_immutable_stdlib_path(path: &Path) -> bool {
868    path.to_str()
869        .is_some_and(|path| path.starts_with("<stdlib>/") || path.starts_with("<std>/"))
870}
871
872fn needs_imported_enum_candidates(program: &[harn_parser::SNode]) -> bool {
873    harn_parser::visit::contains_identifier_enum_pattern(program)
874}
875
876fn needs_imported_symbol_projection(program: &[harn_parser::SNode]) -> bool {
877    needs_imported_enum_candidates(program) || needs_imported_source_callable_names(program)
878}
879
880fn needs_imported_source_callable_names(program: &[harn_parser::SNode]) -> bool {
881    let has_wildcard_import = program.iter().any(|node| match &node.node {
882        harn_parser::Node::ImportDecl { .. } => true,
883        harn_parser::Node::AttributedDecl { inner, .. } => {
884            matches!(&inner.node, harn_parser::Node::ImportDecl { .. })
885        }
886        _ => false,
887    });
888    if !has_wildcard_import {
889        return false;
890    }
891    let mut needs_projection = false;
892    harn_parser::visit::walk_program(program, &mut |node| {
893        if let harn_parser::Node::FunctionCall { name, .. } = &node.node {
894            needs_projection |= harn_parser::builtin_signatures::is_builtin(name);
895        }
896    });
897    needs_projection
898}
899
900fn parse_module_source(
901    source_path: &Path,
902    source: &str,
903) -> Result<Vec<harn_parser::SNode>, VmError> {
904    let mut lexer = harn_lexer::Lexer::new(source);
905    let tokens = lexer.tokenize().map_err(|e| {
906        VmError::Runtime(format!(
907            "Import lex error in {}: {e}",
908            source_path.display()
909        ))
910    })?;
911    let mut parser = harn_parser::Parser::new(tokens);
912    parser.parse().map_err(|e| {
913        VmError::Runtime(format!(
914            "Import parse error in {}: {e}",
915            source_path.display()
916        ))
917    })
918}
919
920/// Parse and compile a source-backed module when the caller already has the
921/// module graph's typed enum-import projection. This keeps precompile/pack
922/// from rebuilding the graph separately for the entry chunk and module
923/// artifact.
924pub fn compile_module_artifact_from_source_with_imported_enums(
925    source_path: &Path,
926    source: &str,
927    imported_enum_candidates: impl IntoIterator<Item = String>,
928) -> Result<ModuleArtifact, VmError> {
929    let program = parse_module_source(source_path, source)?;
930    let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
931    let imported_symbols = imported_symbol_projection_for_program(source_path, source, &program);
932    compile_module_artifact_with_imported_symbols(
933        &program,
934        Some(source_path.display().to_string()),
935        &imported_enum_candidates,
936        &imported_symbols.source_callable_names,
937    )
938}
939
940/// Compile a source-backed module from one already-resolved module-graph
941/// projection. Closed-program linkers use this entry point because their
942/// runtime paths need not exist on the host filesystem.
943pub fn compile_module_artifact_from_source_with_imported_symbols(
944    source_path: &Path,
945    source: &str,
946    imported_enum_candidates: impl IntoIterator<Item = String>,
947    imported_source_callable_names: impl IntoIterator<Item = String>,
948) -> Result<ModuleArtifact, VmError> {
949    let context =
950        ModuleCompilationContext::new(imported_enum_candidates, imported_source_callable_names);
951    compile_module_artifact_from_source_with_context(source_path, source, &context)
952}
953
954pub fn compile_module_artifact_from_source_with_context(
955    source_path: &Path,
956    source: &str,
957    context: &ModuleCompilationContext,
958) -> Result<ModuleArtifact, VmError> {
959    let program = parse_module_source(source_path, source)?;
960    compile_module_artifact_with_imported_symbols(
961        &program,
962        Some(source_path.display().to_string()),
963        context.enum_candidates(),
964        context.source_callable_names(),
965    )
966}
967
968/// Compile source that is embedded in this Harn binary as part of the stdlib.
969///
970/// The embedded-stdlib cache has its own identity domain. Keeping this entry
971/// point separate prevents filesystem source from requesting runtime-internal
972/// builtin authority while allowing stdlib wrappers to use their private
973/// implementation builtins.
974pub(crate) fn compile_embedded_stdlib_module_artifact_from_source_with_context(
975    source_path: &Path,
976    source: &str,
977    context: &ModuleCompilationContext,
978) -> Result<ModuleArtifact, VmError> {
979    let program = parse_module_source(source_path, source)?;
980    compile_module_artifact_with_options(
981        &program,
982        Some(source_path.display().to_string()),
983        context.enum_candidates(),
984        context.source_callable_names(),
985        ModuleProvenance::EmbeddedStdlib,
986        crate::CompilerOptions::embedded_stdlib(),
987    )
988}
989
990#[cfg(test)]
991mod tests {
992    use std::path::Path;
993
994    use harn_lexer::Lexer;
995    use harn_parser::Parser;
996
997    use super::{
998        compile_embedded_stdlib_module_artifact_from_source_with_context, compile_module_artifact,
999        compile_module_artifact_from_source, compile_privileged_wire_module_artifact_from_source,
1000        needs_imported_enum_candidates, parse_module_source, ModuleCompilationContext,
1001        ModuleImportBinding, ModuleProvenance,
1002    };
1003    use crate::chunk::Constant;
1004
1005    #[test]
1006    fn module_init_schema_of_uses_full_program_aliases() {
1007        let source = r"
1008pub type Item = {id: string}
1009const ITEM_SCHEMA: Schema<Item> = schema_of(Item)
1010";
1011        let mut lexer = Lexer::new(source);
1012        let tokens = lexer.tokenize().unwrap();
1013        let mut parser = Parser::new(tokens);
1014        let program = parser.parse().unwrap();
1015        let artifact = compile_module_artifact(&program, None).unwrap();
1016        let constants = &artifact.init_chunk.expect("init chunk").constants;
1017        let strings = constants
1018            .iter()
1019            .filter_map(|constant| match constant {
1020                Constant::String(value) => Some(value.as_str()),
1021                _ => None,
1022            })
1023            .collect::<Vec<_>>();
1024        assert!(strings.contains(&"id"), "{strings:?}");
1025        assert!(!strings.contains(&"Item"), "{strings:?}");
1026    }
1027
1028    #[test]
1029    fn type_only_modules_use_a_separate_schema_initializer() {
1030        let source = r"
1031pub type UserShape = {name: string, active?: bool}
1032pub type UserList = list<UserShape>
1033";
1034
1035        let artifact =
1036            compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
1037                .expect("module compiles");
1038
1039        assert!(
1040            artifact.init_chunk.is_none(),
1041            "erased type aliases must not inflate module init bytecode"
1042        );
1043        assert!(artifact.public_type_names.contains("UserShape"));
1044        assert!(artifact.public_type_names.contains("UserList"));
1045        assert_eq!(artifact.type_schema_init_chunks.len(), 2);
1046    }
1047
1048    #[test]
1049    fn specialization_prunes_dead_pipeline_struct_and_enum_exports() {
1050        let source = r#"
1051pub enum KeptStatus { Ready }
1052pub enum DeadStatus { Gone }
1053pub struct KeptConfig { value: int }
1054pub struct DeadConfig { value: string }
1055pub pipeline kept_pipeline(harness: Harness) { return KeptConfig({value: 7}) }
1056pub pipeline dead_pipeline(harness: Harness) { return DeadConfig({value: "dead"}) }
1057"#;
1058        let source_path = Path::new("<test>/declarations.harn");
1059        let parsed = parse_module_source(source_path, source).expect("module parses");
1060        let full = compile_module_artifact(&parsed, Some(source_path.display().to_string()))
1061            .expect("module compiles");
1062        let selected = super::specialize_module_artifact(
1063            &parsed,
1064            Some(source_path.display().to_string()),
1065            full,
1066            &harn_modules::ExportDemand::Members(std::collections::BTreeSet::from([
1067                "KeptStatus".to_string(),
1068                "KeptConfig".to_string(),
1069                "kept_pipeline".to_string(),
1070            ])),
1071        )
1072        .expect("specialization succeeds");
1073
1074        assert!(selected.public_exports.contains_key("KeptStatus"));
1075        assert!(selected.public_exports.contains_key("KeptConfig"));
1076        assert!(selected.public_exports.contains_key("kept_pipeline"));
1077        assert!(!selected.public_exports.contains_key("DeadStatus"));
1078        assert!(!selected.public_exports.contains_key("DeadConfig"));
1079        assert!(!selected.public_exports.contains_key("dead_pipeline"));
1080        assert!(selected.functions.contains_key("KeptConfig"));
1081        assert!(selected.functions.contains_key("kept_pipeline"));
1082        assert!(!selected.functions.contains_key("DeadConfig"));
1083        assert!(!selected.functions.contains_key("dead_pipeline"));
1084    }
1085
1086    #[test]
1087    fn nested_namespace_import_retains_static_member_demand() {
1088        let artifact = compile_module_artifact_from_source(
1089            Path::new("<test>/wrapper.harn"),
1090            r#"
1091import * as lib from "./lib"
1092pub fn call() { return lib.greet() }
1093"#,
1094        )
1095        .expect("module compiles");
1096
1097        let ModuleImportBinding::Namespace { alias, demand } = &artifact.imports[0].binding else {
1098            panic!("expected namespace import metadata");
1099        };
1100        assert_eq!(alias, "lib");
1101        assert_eq!(
1102            demand,
1103            &harn_parser::NamespaceDemand::Members(std::collections::BTreeSet::from([
1104                "greet".to_string(),
1105            ]))
1106        );
1107    }
1108
1109    #[test]
1110    fn ordinary_modules_cannot_name_privileged_wire_builtins() {
1111        let source = r#"fn probe() { return host_call("project.scan", {}) }"#;
1112        let error = compile_module_artifact_from_source(Path::new("<test>/user.harn"), source)
1113            .expect_err("a tail call must not acquire wire authority");
1114        assert!(
1115            error.to_string().contains("not callable source API"),
1116            "{error}"
1117        );
1118    }
1119
1120    #[test]
1121    fn embedded_stdlib_can_wrap_runtime_internal_builtins_without_exposing_them() {
1122        let source = r#"pub fn ansi_enabled() { return __ansi_enabled("stdout") }"#;
1123        let context = ModuleCompilationContext::default();
1124        compile_embedded_stdlib_module_artifact_from_source_with_context(
1125            Path::new("<stdlib>/ansi.harn"),
1126            source,
1127            &context,
1128        )
1129        .expect("embedded stdlib wrapper compiles");
1130
1131        let error = compile_module_artifact_from_source(Path::new("<test>/user.harn"), source)
1132            .expect_err("ordinary source must not acquire stdlib authority");
1133        assert!(
1134            error.to_string().contains("not callable source API"),
1135            "{error}"
1136        );
1137    }
1138
1139    #[test]
1140    fn explicit_privileged_compilation_stamps_private_wire_code() {
1141        let artifact = compile_privileged_wire_module_artifact_from_source(
1142            Path::new("<trusted>/wire.harn"),
1143            r#"fn probe() { host_call("project.scan", {}) }"#,
1144        )
1145        .expect("trusted private wire function compiles");
1146        assert_eq!(artifact.provenance, ModuleProvenance::PrivilegedWire);
1147        assert!(artifact.functions.contains_key("probe"));
1148        assert!(artifact.public_exports.is_empty());
1149    }
1150
1151    #[test]
1152    fn privileged_wire_functions_cannot_cross_the_module_boundary() {
1153        let error = compile_privileged_wire_module_artifact_from_source(
1154            Path::new("<trusted>/wire.harn"),
1155            r#"pub fn probe() { host_call("project.scan", {}) }"#,
1156        )
1157        .expect_err("wire closures must not be exportable");
1158        assert!(
1159            error
1160                .to_string()
1161                .contains("only explicit capability-value bindings"),
1162            "{error}"
1163        );
1164    }
1165
1166    #[test]
1167    fn privileged_wire_modules_cannot_reexport_imports() {
1168        let error = compile_privileged_wire_module_artifact_from_source(
1169            Path::new("<trusted>/wire.harn"),
1170            r#"pub import { probe } from "./other""#,
1171        )
1172        .expect_err("wire authority must be non-reexportable");
1173        assert!(
1174            error.to_string().contains("cannot re-export imports"),
1175            "{error}"
1176        );
1177    }
1178
1179    #[test]
1180    fn schema_initializer_keeps_imported_alias_lookup_and_source() {
1181        let source = r#"
1182import { External } from "./external"
1183pub type Wrapped = {value: External}
1184"#;
1185        let source_path = Path::new("<test>/wrapped.harn");
1186        let artifact =
1187            compile_module_artifact_from_source(source_path, source).expect("module compiles");
1188        let chunk = artifact
1189            .type_schema_init_chunks
1190            .into_iter()
1191            .next()
1192            .expect("schema initializer");
1193        assert_eq!(chunk.source_file.as_deref(), Some("<test>/wrapped.harn"));
1194        assert!(chunk
1195            .constants
1196            .iter()
1197            .any(|constant| matches!(constant, Constant::String(value) if value == "External")));
1198    }
1199
1200    #[test]
1201    fn imported_enum_graph_lookup_is_lazy_for_plain_modules() {
1202        let plain = parse_module_source(
1203            Path::new("<test>/plain.harn"),
1204            r#"
1205import { helper } from "./support"
1206pub fn run() -> int { return helper(1) }
1207"#,
1208        )
1209        .expect("plain module parses");
1210        assert!(!needs_imported_enum_candidates(&plain));
1211
1212        let qualified = parse_module_source(
1213            Path::new("<test>/qualified.harn"),
1214            r#"
1215import { Status } from "./status"
1216pub fn run(value: Status) {
1217  match value {
1218    Status.Ready -> { return 1 }
1219    _ -> { return 0 }
1220  }
1221}
1222"#,
1223        )
1224        .expect("qualified module parses");
1225        assert!(needs_imported_enum_candidates(&qualified));
1226    }
1227
1228    #[test]
1229    fn private_declarations_do_not_expand_module_init() {
1230        let artifact = compile_module_artifact_from_source(
1231            Path::new("<test>/private-declarations.harn"),
1232            r"
1233enum PrivateStatus { Ready }
1234struct PrivateConfig { value: int }
1235pub fn run() { return PrivateStatus.Ready }
1236",
1237        )
1238        .expect("private declarations compile");
1239
1240        assert!(artifact.init_chunk.is_none());
1241        assert!(artifact.functions.contains_key("PrivateConfig"));
1242        assert!(!artifact.public_exports.contains_key("PrivateStatus"));
1243        assert!(!artifact.public_exports.contains_key("PrivateConfig"));
1244    }
1245}