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(Debug, Serialize, Deserialize)]
51pub struct ModuleImportSpec {
52    pub path: String,
53    pub selected_names: Option<Vec<String>>,
54    /// When set, bind `import * as alias from path` as a namespace dict
55    /// instead of flattening exports into the caller.
56    #[serde(default)]
57    pub namespace_alias: Option<String>,
58    pub is_pub: bool,
59}
60
61/// Serializable compile artifact for one `.harn` module. The runtime
62/// turns this into a loaded module by replaying [`init_chunk`](Self::init_chunk)
63/// into a fresh env, minting closures for each entry in
64/// [`functions`](Self::functions), and re-issuing every nested
65/// [`imports`](Self::imports).
66#[derive(Debug, Serialize, Deserialize)]
67pub struct ModuleArtifact {
68    #[serde(default)]
69    pub provenance: ModuleProvenance,
70    pub imports: Vec<ModuleImportSpec>,
71    /// Cached bytecode that materializes exported type aliases after imports
72    /// are bound and before value initialization runs.
73    pub type_schema_init_chunk: Option<CachedChunk>,
74    pub init_chunk: Option<CachedChunk>,
75    pub functions: BTreeMap<String, CachedCompiledFunction>,
76    /// The public declaration contract shared with `harn-modules`. Each name
77    /// carries its source declaration kind so the loader can choose a closure,
78    /// initialized value, schema, or type-only projection without maintaining
79    /// a second AST export table.
80    pub public_exports: BTreeMap<String, DefKind>,
81    /// Public declarations whose runtime value is produced by replaying
82    /// [`init_chunk`](Self::init_chunk), rather than the precompiled function
83    /// table. This includes bindings, enums, tools, skills, and eval packs.
84    pub public_value_names: HashSet<String>,
85    /// Names of erased public type declarations (`type` and `interface`). They
86    /// carry no runtime value of their own, but importers may still name them
87    /// in selective imports. Public structs and enums are excluded because
88    /// they export runtime constructors/namespaces.
89    pub public_type_names: HashSet<String>,
90}
91
92impl ModuleArtifact {
93    /// Bind relocatable cached bytecode to the source path used by this load.
94    ///
95    /// Module artifacts may move beside their source (`harn precompile`) or
96    /// inside a package. Source paths are diagnostic/debug context, not a
97    /// compilation input, so deserialize once and stamp every nested chunk at
98    /// the load boundary instead of duplicating otherwise-identical artifacts.
99    pub(crate) fn bind_source_file(&mut self, source_path: &Path) {
100        let source_file = source_path.display().to_string();
101        if let Some(chunk) = &mut self.type_schema_init_chunk {
102            bind_chunk_source_file(chunk, &source_file);
103        }
104        if let Some(chunk) = &mut self.init_chunk {
105            bind_chunk_source_file(chunk, &source_file);
106        }
107        for function in self.functions.values_mut() {
108            bind_chunk_source_file(&mut function.chunk, &source_file);
109        }
110    }
111}
112
113fn bind_chunk_source_file(chunk: &mut CachedChunk, source_file: &str) {
114    chunk.source_file = Some(source_file.to_string());
115    for function in &mut chunk.functions {
116        bind_chunk_source_file(&mut function.chunk, source_file);
117    }
118}
119
120/// Compile a parsed `.harn` module into the serializable artifact shape.
121/// Pure compilation — no I/O, no execution. Used by both the runtime
122/// import path (`crates/harn-vm/src/vm/modules.rs`) and the
123/// `harn precompile` CLI subcommand.
124pub fn compile_module_artifact(
125    program: &[harn_parser::SNode],
126    module_source_file: Option<String>,
127) -> Result<ModuleArtifact, VmError> {
128    let imported_enum_candidates = module_source_file
129        .as_deref()
130        .filter(|_| needs_imported_enum_candidates(program))
131        .and_then(|path| {
132            harn_modules::build(&[Path::new(path).to_path_buf()])
133                .imported_names_by_kind_for_file(Path::new(path), DefKind::Enum)
134        })
135        .unwrap_or_default();
136    compile_module_artifact_with_imported_enums(
137        program,
138        module_source_file,
139        &imported_enum_candidates.into_iter().collect::<Vec<_>>(),
140    )
141}
142
143fn compile_module_artifact_with_imported_enums(
144    program: &[harn_parser::SNode],
145    module_source_file: Option<String>,
146    imported_enum_candidates: &[String],
147) -> Result<ModuleArtifact, VmError> {
148    compile_module_artifact_with_provenance(
149        program,
150        module_source_file,
151        imported_enum_candidates,
152        ModuleProvenance::User,
153    )
154}
155
156fn compile_module_artifact_with_provenance(
157    program: &[harn_parser::SNode],
158    module_source_file: Option<String>,
159    imported_enum_candidates: &[String],
160    provenance: ModuleProvenance,
161) -> Result<ModuleArtifact, VmError> {
162    let imports: Vec<ModuleImportSpec> = program
163        .iter()
164        .filter_map(|node| match &node.node {
165            harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
166                path: path.clone(),
167                selected_names: None,
168                namespace_alias: None,
169                is_pub: *is_pub,
170            }),
171            harn_parser::Node::SelectiveImport {
172                names,
173                path,
174                is_pub,
175            } => Some(ModuleImportSpec {
176                path: path.clone(),
177                selected_names: Some(names.clone()),
178                namespace_alias: None,
179                is_pub: *is_pub,
180            }),
181            harn_parser::Node::NamespaceImport {
182                alias,
183                path,
184                is_pub,
185            } => Some(ModuleImportSpec {
186                path: path.clone(),
187                selected_names: None,
188                namespace_alias: Some(alias.clone()),
189                is_pub: *is_pub,
190            }),
191            _ => None,
192        })
193        .collect();
194
195    if provenance == ModuleProvenance::PrivilegedWire {
196        validate_privileged_wire_surface(program, &imports)?;
197    }
198
199    let compiler = || match provenance {
200        ModuleProvenance::User => crate::Compiler::new(),
201        ModuleProvenance::PrivilegedWire => {
202            crate::Compiler::with_options(crate::CompilerOptions::privileged_wire())
203        }
204        ModuleProvenance::TrustedHostDispatch => {
205            crate::Compiler::with_options(crate::CompilerOptions::privileged_wire())
206        }
207    };
208
209    let init_nodes: Vec<harn_parser::SNode> = program
210        .iter()
211        .filter(|sn| {
212            let inner = match &sn.node {
213                harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
214                _ => sn,
215            };
216            matches!(
217                &inner.node,
218                harn_parser::Node::LetBinding { .. }
219                    | harn_parser::Node::ConstBinding { .. }
220                    // Only public enums need a runtime namespace in an
221                    // imported module. Private enum construction lowers
222                    // directly to `BuildEnum`, just like local construction,
223                    // so materializing a private namespace only adds cold
224                    // module-init work and closures that can never be
225                    // imported.
226                    | harn_parser::Node::EnumDecl { is_pub: true, .. }
227                    | harn_parser::Node::ToolDecl { .. }
228                    | harn_parser::Node::SkillDecl { .. }
229                    | harn_parser::Node::EvalPackDecl { .. }
230            )
231        })
232        .cloned()
233        .collect();
234    let init_chunk = if init_nodes.is_empty() {
235        None
236    } else {
237        let compiler = compiler();
238        Some(
239            compiler
240                .compile_module_init(program, &init_nodes, imported_enum_candidates)
241                .map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
242                .freeze_for_cache(),
243        )
244    };
245
246    let public_exports: BTreeMap<String, DefKind> = program
247        .iter()
248        .flat_map(public_declarations)
249        .map(|export| (export.name, export.kind))
250        .collect();
251    let public_value_names = public_exports
252        .iter()
253        .filter(|(_, kind)| {
254            matches!(
255                kind,
256                DefKind::Variable
257                    | DefKind::Enum
258                    | DefKind::Tool
259                    | DefKind::Skill
260                    | DefKind::EvalPack
261            )
262        })
263        .map(|(name, _)| name.clone())
264        .collect();
265    let public_type_names = public_exports
266        .iter()
267        .filter(|(_, kind)| !kind.has_runtime_value())
268        .map(|(name, _)| name.clone())
269        .collect();
270
271    let mut functions = BTreeMap::new();
272    for node in program {
273        let inner = match &node.node {
274            harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
275            _ => node,
276        };
277        if let harn_parser::Node::StructDecl { name, fields, .. } = &inner.node {
278            // Struct constructors are ordinary module callables. Keeping
279            // them in the artifact function table avoids replaying the
280            // declaration through the module-init chunk while preserving
281            // private struct use and public constructor imports.
282            let constructor = compiler()
283                .compile_struct_constructor(name, fields)
284                .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
285            functions.insert(name.clone(), constructor.freeze_for_cache());
286            continue;
287        }
288        if let harn_parser::Node::Pipeline {
289            name,
290            params,
291            body,
292            extends,
293            ..
294        } = &inner.node
295        {
296            let mut compiler = compiler();
297            compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
298            let pipeline = compiler
299                .compile_pipeline_callable(program, name, params, body, extends.as_deref())
300                .map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
301            functions.insert(name.clone(), pipeline.freeze_for_cache());
302            continue;
303        }
304        let harn_parser::Node::FnDecl {
305            name,
306            type_params,
307            params,
308            body,
309            ..
310        } = &inner.node
311        else {
312            continue;
313        };
314
315        let mut compiler = compiler();
316        compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
317        compiler.prepare_module_context(program);
318        let func_chunk = compiler
319            .compile_fn_body(type_params, params, body, module_source_file.clone())
320            .map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
321        functions.insert(name.clone(), func_chunk.freeze_for_cache());
322    }
323
324    let type_schema_init_chunk =
325        crate::Compiler::compile_public_type_schema_initializers(program, module_source_file)
326            .map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
327            .map(|chunk| chunk.freeze_for_cache());
328
329    Ok(ModuleArtifact {
330        provenance,
331        imports,
332        type_schema_init_chunk,
333        init_chunk,
334        functions,
335        public_exports,
336        public_value_names,
337        public_type_names,
338    })
339}
340
341fn validate_privileged_wire_surface(
342    program: &[harn_parser::SNode],
343    imports: &[ModuleImportSpec],
344) -> Result<(), VmError> {
345    if imports.iter().any(|import| import.is_pub) {
346        return Err(VmError::Runtime(
347            "Privileged wire modules cannot re-export imports".to_string(),
348        ));
349    }
350    for export in program.iter().flat_map(public_declarations) {
351        if export.kind.has_runtime_value() && export.kind != DefKind::Variable {
352            return Err(VmError::Runtime(format!(
353                "Privileged wire module export `{}` is a {:?}; only explicit capability-value bindings may cross the wire boundary",
354                export.name, export.kind
355            )));
356        }
357    }
358    Ok(())
359}
360
361/// Lex + parse + [`compile_module_artifact`] in one call. Used when the
362/// caller already has the raw source bytes and wants the artifact in one
363/// step.
364pub fn compile_module_artifact_from_source(
365    source_path: &Path,
366    source: &str,
367) -> Result<ModuleArtifact, VmError> {
368    let program = parse_module_source(source_path, source)?;
369    let imported_enum_candidates =
370        imported_enum_candidates_for_program(source_path, source, &program);
371    compile_module_artifact_with_imported_enums(
372        &program,
373        Some(source_path.display().to_string()),
374        &imported_enum_candidates,
375    )
376}
377
378/// Compile a trusted embedder-owned module that may call
379/// [`BuiltinExposure::PrivilegedWire`](harn_builtin_meta::BuiltinExposure)
380/// primitives.
381///
382/// The resulting module may export only initialized capability values and
383/// erased types. Runtime instantiation validates those values again, so
384/// closures, dictionaries, and arbitrary host results cannot smuggle wire
385/// authority into user modules.
386pub fn compile_privileged_wire_module_artifact_from_source(
387    source_path: &Path,
388    source: &str,
389) -> Result<ModuleArtifact, VmError> {
390    let program = parse_module_source(source_path, source)?;
391    let imported_enum_candidates =
392        imported_enum_candidates_for_program(source_path, source, &program);
393    compile_module_artifact_with_provenance(
394        &program,
395        Some(source_path.display().to_string()),
396        &imported_enum_candidates,
397        ModuleProvenance::PrivilegedWire,
398    )
399}
400
401/// Compile one module in a Rust embedder-owned host-dispatch graph.
402///
403/// The runtime loader is responsible for keeping this provenance outside the
404/// ordinary import/cache path and returning only the host-selected callable.
405pub fn compile_trusted_host_dispatch_module_artifact_from_source(
406    source_path: &Path,
407    source: &str,
408) -> Result<ModuleArtifact, VmError> {
409    let program = parse_module_source(source_path, source)?;
410    let imported_enum_candidates =
411        imported_enum_candidates_for_program(source_path, source, &program);
412    compile_module_artifact_with_provenance(
413        &program,
414        Some(source_path.display().to_string()),
415        &imported_enum_candidates,
416        ModuleProvenance::TrustedHostDispatch,
417    )
418}
419
420/// Resolve imported enum names only for modules whose match patterns can use
421/// them. Ordinary property access is runtime lookup and does not need a graph
422/// walk; avoiding it keeps uncached module compilation independent of the
423/// size of unrelated import closures.
424fn imported_enum_candidates_for_program(
425    source_path: &Path,
426    source: &str,
427    program: &[harn_parser::SNode],
428) -> Vec<String> {
429    if !needs_imported_enum_candidates(program) {
430        return Vec::new();
431    }
432    let source_hash = *blake3::hash(source.as_bytes()).as_bytes();
433    let cache_key = harn_modules::canonical_path(source_path);
434    let cacheable = is_immutable_stdlib_path(source_path);
435    if cacheable {
436        if let Some((_cached_hash, candidates)) = imported_enum_cache()
437            .lock()
438            .expect("imported enum cache lock poisoned")
439            .get(&cache_key)
440            .filter(|(cached_hash, _)| *cached_hash == source_hash)
441        {
442            return candidates.clone();
443        }
444    }
445
446    // A graph walk is needed to resolve wildcard and re-exported enums, but
447    // the result describes every module in that closure. Publish all those
448    // projections at once so loading a large stdlib does not rebuild the same
449    // reachable graph once per module artifact.
450    let graph = harn_modules::build_with_source(source_path, source);
451    if !cacheable {
452        return sorted_imported_enum_candidates(&graph, source_path);
453    }
454    let mut projections = Vec::new();
455    for path in graph.module_paths() {
456        let module_source = if path == cache_key {
457            Some(source.to_string())
458        } else {
459            harn_modules::read_module_source(&path).or_else(|| std::fs::read_to_string(&path).ok())
460        };
461        let Some(module_source) = module_source else {
462            continue;
463        };
464        let candidates = sorted_imported_enum_candidates(&graph, &path);
465        projections.push((
466            path,
467            (
468                *blake3::hash(module_source.as_bytes()).as_bytes(),
469                candidates,
470            ),
471        ));
472    }
473    let mut cache = imported_enum_cache()
474        .lock()
475        .expect("imported enum cache lock poisoned");
476    for (path, projection) in projections {
477        if is_immutable_stdlib_path(&path) {
478            cache.insert(path, projection);
479        }
480    }
481    cache
482        .get(&cache_key)
483        .filter(|(cached_hash, _)| *cached_hash == source_hash)
484        .map(|(_, candidates)| candidates.clone())
485        .unwrap_or_default()
486}
487
488fn sorted_imported_enum_candidates(
489    graph: &harn_modules::ModuleGraph,
490    source_path: &Path,
491) -> Vec<String> {
492    let mut candidates = graph
493        .imported_names_by_kind_for_file(source_path, DefKind::Enum)
494        .unwrap_or_default()
495        .into_iter()
496        .collect::<Vec<_>>();
497    candidates.sort_unstable();
498    candidates
499}
500
501fn is_immutable_stdlib_path(path: &Path) -> bool {
502    path.to_str()
503        .is_some_and(|path| path.starts_with("<stdlib>/") || path.starts_with("<std>/"))
504}
505
506fn needs_imported_enum_candidates(program: &[harn_parser::SNode]) -> bool {
507    harn_parser::visit::contains_identifier_enum_pattern(program)
508}
509
510fn parse_module_source(
511    source_path: &Path,
512    source: &str,
513) -> Result<Vec<harn_parser::SNode>, VmError> {
514    let mut lexer = harn_lexer::Lexer::new(source);
515    let tokens = lexer.tokenize().map_err(|e| {
516        VmError::Runtime(format!(
517            "Import lex error in {}: {e}",
518            source_path.display()
519        ))
520    })?;
521    let mut parser = harn_parser::Parser::new(tokens);
522    parser.parse().map_err(|e| {
523        VmError::Runtime(format!(
524            "Import parse error in {}: {e}",
525            source_path.display()
526        ))
527    })
528}
529
530/// Parse and compile a source-backed module when the caller already has the
531/// module graph's typed enum-import projection. This keeps precompile/pack
532/// from rebuilding the graph separately for the entry chunk and module
533/// artifact.
534pub fn compile_module_artifact_from_source_with_imported_enums(
535    source_path: &Path,
536    source: &str,
537    imported_enum_candidates: impl IntoIterator<Item = String>,
538) -> Result<ModuleArtifact, VmError> {
539    let program = parse_module_source(source_path, source)?;
540    let imported_enum_candidates = imported_enum_candidates.into_iter().collect::<Vec<_>>();
541    compile_module_artifact_with_imported_enums(
542        &program,
543        Some(source_path.display().to_string()),
544        &imported_enum_candidates,
545    )
546}
547
548#[cfg(test)]
549mod tests {
550    use std::path::Path;
551
552    use harn_lexer::Lexer;
553    use harn_parser::Parser;
554
555    use super::{
556        compile_module_artifact, compile_module_artifact_from_source,
557        compile_privileged_wire_module_artifact_from_source, needs_imported_enum_candidates,
558        parse_module_source, ModuleProvenance,
559    };
560    use crate::chunk::Constant;
561
562    #[test]
563    fn module_init_schema_of_uses_full_program_aliases() {
564        let source = r"
565pub type Item = {id: string}
566const ITEM_SCHEMA: Schema<Item> = schema_of(Item)
567";
568        let mut lexer = Lexer::new(source);
569        let tokens = lexer.tokenize().unwrap();
570        let mut parser = Parser::new(tokens);
571        let program = parser.parse().unwrap();
572        let artifact = compile_module_artifact(&program, None).unwrap();
573        let constants = &artifact.init_chunk.expect("init chunk").constants;
574        let strings = constants
575            .iter()
576            .filter_map(|constant| match constant {
577                Constant::String(value) => Some(value.as_str()),
578                _ => None,
579            })
580            .collect::<Vec<_>>();
581        assert!(strings.contains(&"id"), "{strings:?}");
582        assert!(!strings.contains(&"Item"), "{strings:?}");
583    }
584
585    #[test]
586    fn type_only_modules_use_a_separate_schema_initializer() {
587        let source = r"
588pub type UserShape = {name: string, active?: bool}
589pub type UserList = list<UserShape>
590";
591
592        let artifact =
593            compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
594                .expect("module compiles");
595
596        assert!(
597            artifact.init_chunk.is_none(),
598            "erased type aliases must not inflate module init bytecode"
599        );
600        assert!(artifact.public_type_names.contains("UserShape"));
601        assert!(artifact.public_type_names.contains("UserList"));
602        assert!(artifact.type_schema_init_chunk.is_some());
603    }
604
605    #[test]
606    fn ordinary_modules_cannot_name_privileged_wire_builtins() {
607        let error = compile_module_artifact_from_source(
608            Path::new("<test>/user.harn"),
609            r#"fn probe() { host_call("project.scan", {}) }"#,
610        )
611        .expect_err("ordinary source must not acquire wire authority");
612        assert!(
613            error.to_string().contains("not callable source API"),
614            "{error}"
615        );
616    }
617
618    #[test]
619    fn explicit_privileged_compilation_stamps_private_wire_code() {
620        let artifact = compile_privileged_wire_module_artifact_from_source(
621            Path::new("<trusted>/wire.harn"),
622            r#"fn probe() { host_call("project.scan", {}) }"#,
623        )
624        .expect("trusted private wire function compiles");
625        assert_eq!(artifact.provenance, ModuleProvenance::PrivilegedWire);
626        assert!(artifact.functions.contains_key("probe"));
627        assert!(artifact.public_exports.is_empty());
628    }
629
630    #[test]
631    fn privileged_wire_functions_cannot_cross_the_module_boundary() {
632        let error = compile_privileged_wire_module_artifact_from_source(
633            Path::new("<trusted>/wire.harn"),
634            r#"pub fn probe() { host_call("project.scan", {}) }"#,
635        )
636        .expect_err("wire closures must not be exportable");
637        assert!(
638            error
639                .to_string()
640                .contains("only explicit capability-value bindings"),
641            "{error}"
642        );
643    }
644
645    #[test]
646    fn privileged_wire_modules_cannot_reexport_imports() {
647        let error = compile_privileged_wire_module_artifact_from_source(
648            Path::new("<trusted>/wire.harn"),
649            r#"pub import { probe } from "./other""#,
650        )
651        .expect_err("wire authority must be non-reexportable");
652        assert!(
653            error.to_string().contains("cannot re-export imports"),
654            "{error}"
655        );
656    }
657
658    #[test]
659    fn schema_initializer_keeps_imported_alias_lookup_and_source() {
660        let source = r#"
661import { External } from "./external"
662pub type Wrapped = {value: External}
663"#;
664        let source_path = Path::new("<test>/wrapped.harn");
665        let artifact =
666            compile_module_artifact_from_source(source_path, source).expect("module compiles");
667        let chunk = artifact.type_schema_init_chunk.expect("schema initializer");
668        assert_eq!(chunk.source_file.as_deref(), Some("<test>/wrapped.harn"));
669        assert!(chunk
670            .constants
671            .iter()
672            .any(|constant| matches!(constant, Constant::String(value) if value == "External")));
673    }
674
675    #[test]
676    fn imported_enum_graph_lookup_is_lazy_for_plain_modules() {
677        let plain = parse_module_source(
678            Path::new("<test>/plain.harn"),
679            r#"
680import { helper } from "./support"
681pub fn run() -> int { return helper(1) }
682"#,
683        )
684        .expect("plain module parses");
685        assert!(!needs_imported_enum_candidates(&plain));
686
687        let qualified = parse_module_source(
688            Path::new("<test>/qualified.harn"),
689            r#"
690import { Status } from "./status"
691pub fn run(value: Status) {
692  match value {
693    Status.Ready -> { return 1 }
694    _ -> { return 0 }
695  }
696}
697"#,
698        )
699        .expect("qualified module parses");
700        assert!(needs_imported_enum_candidates(&qualified));
701    }
702
703    #[test]
704    fn private_declarations_do_not_expand_module_init() {
705        let artifact = compile_module_artifact_from_source(
706            Path::new("<test>/private-declarations.harn"),
707            r"
708enum PrivateStatus { Ready }
709struct PrivateConfig { value: int }
710pub fn run() { return PrivateStatus.Ready }
711",
712        )
713        .expect("private declarations compile");
714
715        assert!(artifact.init_chunk.is_none());
716        assert!(artifact.functions.contains_key("PrivateConfig"));
717        assert!(!artifact.public_exports.contains_key("PrivateStatus"));
718        assert!(!artifact.public_exports.contains_key("PrivateConfig"));
719    }
720}