Skip to main content

harn_kernel/compiler/
module.rs

1//! Host-independent compilation of one module for a portable package.
2//!
3//! The full VM has a richer module loader, but the bytecode that a module
4//! owns is the same compiler output used by the portable kernel.  Keeping this
5//! small image builder here lets native and browser package adapters share the
6//! compiler without making the kernel depend on paths, files, or the async VM.
7
8use std::collections::BTreeMap;
9
10use harn_parser::{Node, SNode};
11use serde::{Deserialize, Serialize};
12
13use crate::{Chunk, CompiledFunction};
14
15use super::{peel_node, CompileError, Compiler};
16
17/// A resolved import edge in a portable package.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct PortableImport {
21    /// The spelling used in source, retained for diagnostics.
22    pub path: String,
23    /// Stable package-local module identifier selected by the host linker.
24    pub target: String,
25    pub selected_names: Option<Vec<String>>,
26    pub namespace_alias: Option<String>,
27    pub is_pub: bool,
28}
29
30/// The small declaration-kind projection needed at the runtime export seam.
31/// The module graph remains the authority for resolving and validating this
32/// projection; the kernel does not inspect filesystem paths or invent a
33/// second visibility table.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum PortableExportKind {
37    Function,
38    Pipeline,
39    Tool,
40    Skill,
41    EvalPack,
42    Struct,
43    Enum,
44    Interface,
45    Type,
46    Variable,
47}
48
49impl PortableExportKind {
50    pub const fn has_runtime_value(self) -> bool {
51        !matches!(self, Self::Interface | Self::Type)
52    }
53}
54
55/// JSON-friendly source projection for a package that has already been
56/// resolved and typechecked by a host linker. The kernel deliberately accepts
57/// stable module IDs and resolved import targets rather than filesystem paths;
58/// this is the same data shape a browser worker can receive from a build
59/// service without gaining loader authority.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct PortableSourceModule {
63    pub id: String,
64    pub source: String,
65    #[serde(default)]
66    pub imports: Vec<PortableImport>,
67    #[serde(default)]
68    pub exports: BTreeMap<String, PortableExportKind>,
69    #[serde(default)]
70    pub imported_enum_candidates: Vec<String>,
71    #[serde(default)]
72    pub source_file: Option<String>,
73}
74
75/// JSON-friendly source package manifest. `rootImports` and each module's
76/// `imports` are linker output, not a second import parser: native hosts can
77/// serialize the same projection produced by `harn-modules`, while Wasm only
78/// parses source into the canonical Harn AST and hands it to the one compiler.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80#[serde(rename_all = "camelCase")]
81pub struct PortableSourcePackage {
82    pub root_source: String,
83    #[serde(default)]
84    pub root_imports: Vec<PortableImport>,
85    #[serde(default)]
86    pub modules: Vec<PortableSourceModule>,
87}
88
89/// Compiled, host-independent image of a module.  Chunks are still immutable
90/// compiler output; a runtime mints fresh environments and closures for each
91/// execution, exactly like the native module loader.
92#[derive(Debug, Clone)]
93pub struct CompiledPortableModule {
94    pub id: String,
95    pub imports: Vec<PortableImport>,
96    pub init: Option<Chunk>,
97    pub functions: BTreeMap<String, CompiledFunction>,
98    pub exports: BTreeMap<String, PortableExportKind>,
99}
100
101impl Compiler {
102    /// Compile a module's initialization and callable declarations using the
103    /// canonical compiler context.  This is deliberately pure: callers own
104    /// source loading, graph resolution, and export policy.
105    pub fn compile_portable_module(
106        mut self,
107        id: impl Into<String>,
108        program: &[SNode],
109        imports: Vec<PortableImport>,
110        exports: BTreeMap<String, PortableExportKind>,
111        imported_enum_candidates: &[String],
112        source_file: Option<String>,
113    ) -> Result<CompiledPortableModule, CompileError> {
114        self.prepare_module_context(program);
115        self.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
116
117        let init_nodes: Vec<SNode> = program
118            .iter()
119            .filter(|sn| {
120                let inner = peel_node(sn);
121                matches!(
122                    inner,
123                    Node::LetBinding { .. }
124                        | Node::ConstBinding { .. }
125                        | Node::EnumDecl { is_pub: true, .. }
126                        | Node::ToolDecl { .. }
127                        | Node::SkillDecl { .. }
128                        | Node::EvalPackDecl { .. }
129                )
130            })
131            .cloned()
132            .collect();
133        let init = if init_nodes.is_empty() {
134            None
135        } else {
136            Some(Compiler::with_options(self.options).compile_module_init(
137                program,
138                &init_nodes,
139                imported_enum_candidates,
140            )?)
141        };
142
143        let mut functions = BTreeMap::new();
144        for node in program {
145            let inner = peel_node(node);
146            match inner {
147                Node::StructDecl { name, fields, .. } => {
148                    let constructor = self.compile_struct_constructor(name, fields)?;
149                    functions.insert(name.clone(), constructor);
150                }
151                Node::Pipeline {
152                    name,
153                    params,
154                    body,
155                    extends,
156                    ..
157                } => {
158                    let function = self.compile_pipeline_callable(
159                        program,
160                        name,
161                        params,
162                        body,
163                        extends.as_deref(),
164                    )?;
165                    functions.insert(name.clone(), function);
166                }
167                Node::FnDecl {
168                    name,
169                    type_params,
170                    params,
171                    body,
172                    ..
173                } => {
174                    let mut compiler = Compiler::with_options(self.options);
175                    compiler.prepare_module_context(program);
176                    compiler.add_imported_enum_candidates(imported_enum_candidates.iter().cloned());
177                    let mut function =
178                        compiler.compile_fn_body(type_params, params, body, source_file.clone())?;
179                    // `compile_fn_body` is also used for anonymous nested
180                    // closures, so it intentionally leaves the display name
181                    // empty. A module function is an exported artifact
182                    // callable and must carry its stable declaration name.
183                    function.name = name.clone();
184                    functions.insert(name.clone(), function);
185                }
186                _ => {}
187            }
188        }
189
190        Ok(CompiledPortableModule {
191            id: id.into(),
192            imports,
193            init,
194            functions,
195            exports,
196        })
197    }
198}