Skip to main content

harn_vm/
module_artifact.rs

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