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