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