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