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