use std::collections::{BTreeMap, HashSet};
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::chunk::{CachedChunk, CachedCompiledFunction};
use crate::value::VmError;
#[derive(Debug, Serialize, Deserialize)]
pub struct ModuleImportSpec {
pub path: String,
pub selected_names: Option<Vec<String>>,
pub is_pub: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ModuleArtifact {
pub imports: Vec<ModuleImportSpec>,
pub type_schema_init_chunk: Option<CachedChunk>,
pub init_chunk: Option<CachedChunk>,
pub functions: BTreeMap<String, CachedCompiledFunction>,
pub public_names: HashSet<String>,
pub public_value_names: HashSet<String>,
pub public_type_names: HashSet<String>,
}
impl ModuleArtifact {
pub(crate) fn bind_source_file(&mut self, source_path: &Path) {
let source_file = source_path.display().to_string();
if let Some(chunk) = &mut self.type_schema_init_chunk {
bind_chunk_source_file(chunk, &source_file);
}
if let Some(chunk) = &mut self.init_chunk {
bind_chunk_source_file(chunk, &source_file);
}
for function in self.functions.values_mut() {
bind_chunk_source_file(&mut function.chunk, &source_file);
}
}
}
fn bind_chunk_source_file(chunk: &mut CachedChunk, source_file: &str) {
chunk.source_file = Some(source_file.to_string());
for function in &mut chunk.functions {
bind_chunk_source_file(&mut function.chunk, source_file);
}
}
pub fn compile_module_artifact(
program: &[harn_parser::SNode],
module_source_file: Option<String>,
) -> Result<ModuleArtifact, VmError> {
let imports = program
.iter()
.filter_map(|node| match &node.node {
harn_parser::Node::ImportDecl { path, is_pub } => Some(ModuleImportSpec {
path: path.clone(),
selected_names: None,
is_pub: *is_pub,
}),
harn_parser::Node::SelectiveImport {
names,
path,
is_pub,
} => Some(ModuleImportSpec {
path: path.clone(),
selected_names: Some(names.clone()),
is_pub: *is_pub,
}),
_ => None,
})
.collect();
let init_nodes: Vec<harn_parser::SNode> = program
.iter()
.filter(|sn| {
let inner = match &sn.node {
harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
_ => sn,
};
matches!(
&inner.node,
harn_parser::Node::LetBinding { .. } | harn_parser::Node::ConstBinding { .. }
)
})
.cloned()
.collect();
let init_chunk = if init_nodes.is_empty() {
None
} else {
let mut compiler = crate::Compiler::new();
compiler.seed_module_catalog(program);
Some(
compiler
.compile(&init_nodes)
.map_err(|e| VmError::Runtime(format!("Import init compile error: {e}")))?
.freeze_for_cache(),
)
};
let mut functions = BTreeMap::new();
let mut public_names = HashSet::new();
let mut public_value_names = HashSet::new();
let mut public_type_names = HashSet::new();
for node in program {
let inner = match &node.node {
harn_parser::Node::AttributedDecl { inner, .. } => inner.as_ref(),
_ => node,
};
match &inner.node {
harn_parser::Node::TypeDecl {
name, is_pub: true, ..
}
| harn_parser::Node::EnumDecl {
name, is_pub: true, ..
}
| harn_parser::Node::InterfaceDecl { name, .. } => {
public_type_names.insert(name.clone());
continue;
}
harn_parser::Node::StructDecl {
name,
fields,
is_pub,
..
} => {
let constructor = crate::Compiler::new()
.compile_struct_constructor(name, fields)
.map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
functions.insert(name.clone(), constructor.freeze_for_cache());
if *is_pub {
public_names.insert(name.clone());
}
continue;
}
_ => {}
}
if let harn_parser::Node::ConstBinding {
pattern,
is_pub: true,
..
}
| harn_parser::Node::LetBinding {
pattern,
is_pub: true,
..
} = &inner.node
{
collect_binding_identifier_names(pattern, &mut public_value_names);
continue;
}
if let harn_parser::Node::Pipeline {
name,
params,
body,
extends,
is_pub,
..
} = &inner.node
{
let pipeline = crate::Compiler::new()
.compile_pipeline_callable(program, name, params, body, extends.as_deref())
.map_err(|error| VmError::Runtime(format!("Import compile error: {error}")))?;
functions.insert(name.clone(), pipeline.freeze_for_cache());
if *is_pub {
public_names.insert(name.clone());
}
continue;
}
let harn_parser::Node::FnDecl {
name,
type_params,
params,
body,
is_pub,
..
} = &inner.node
else {
continue;
};
let mut compiler = crate::Compiler::new();
compiler.seed_module_catalog(program);
let func_chunk = compiler
.compile_fn_body(type_params, params, body, module_source_file.clone())
.map_err(|e| VmError::Runtime(format!("Import compile error: {e}")))?;
functions.insert(name.clone(), func_chunk.freeze_for_cache());
if *is_pub {
public_names.insert(name.clone());
}
}
let type_schema_init_chunk =
crate::Compiler::compile_public_type_schema_initializers(program, module_source_file)
.map_err(|error| VmError::Runtime(format!("Import schema compile error: {error}")))?
.map(|chunk| chunk.freeze_for_cache());
Ok(ModuleArtifact {
imports,
type_schema_init_chunk,
init_chunk,
functions,
public_names,
public_value_names,
public_type_names,
})
}
fn collect_binding_identifier_names(
pattern: &harn_parser::BindingPattern,
out: &mut HashSet<String>,
) {
use harn_parser::BindingPattern;
match pattern {
BindingPattern::Identifier(name) => {
out.insert(name.clone());
}
BindingPattern::Pair(first, second) => {
out.insert(first.clone());
out.insert(second.clone());
}
BindingPattern::List(elements) => {
for element in elements {
out.insert(element.name.clone());
}
}
BindingPattern::Dict(fields) => {
for field in fields {
out.insert(field.alias.clone().unwrap_or_else(|| field.key.clone()));
}
}
}
}
pub fn compile_module_artifact_from_source(
source_path: &Path,
source: &str,
) -> Result<ModuleArtifact, VmError> {
let mut lexer = harn_lexer::Lexer::new(source);
let tokens = lexer.tokenize().map_err(|e| {
VmError::Runtime(format!(
"Import lex error in {}: {e}",
source_path.display()
))
})?;
let mut parser = harn_parser::Parser::new(tokens);
let program = parser.parse().map_err(|e| {
VmError::Runtime(format!(
"Import parse error in {}: {e}",
source_path.display()
))
})?;
compile_module_artifact(&program, Some(source_path.display().to_string()))
}
#[cfg(test)]
mod tests {
use std::path::Path;
use harn_lexer::Lexer;
use harn_parser::Parser;
use super::{compile_module_artifact, compile_module_artifact_from_source};
use crate::chunk::Constant;
#[test]
fn module_init_schema_of_uses_full_program_aliases() {
let source = r"
pub type Item = {id: string}
const ITEM_SCHEMA: Schema<Item> = schema_of(Item)
";
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize().unwrap();
let mut parser = Parser::new(tokens);
let program = parser.parse().unwrap();
let artifact = compile_module_artifact(&program, None).unwrap();
let constants = &artifact.init_chunk.expect("init chunk").constants;
let strings = constants
.iter()
.filter_map(|constant| match constant {
Constant::String(value) => Some(value.as_str()),
_ => None,
})
.collect::<Vec<_>>();
assert!(strings.contains(&"id"), "{strings:?}");
assert!(!strings.contains(&"Item"), "{strings:?}");
}
#[test]
fn type_only_modules_use_a_separate_schema_initializer() {
let source = r"
pub type UserShape = {name: string, active?: bool}
pub type UserList = list<UserShape>
";
let artifact =
compile_module_artifact_from_source(Path::new("<test>/schemas.harn"), source)
.expect("module compiles");
assert!(
artifact.init_chunk.is_none(),
"erased type aliases must not inflate module init bytecode"
);
assert!(artifact.public_type_names.contains("UserShape"));
assert!(artifact.public_type_names.contains("UserList"));
assert!(artifact.type_schema_init_chunk.is_some());
}
#[test]
fn schema_initializer_keeps_imported_alias_lookup_and_source() {
let source = r#"
import { External } from "./external"
pub type Wrapped = {value: External}
"#;
let source_path = Path::new("<test>/wrapped.harn");
let artifact =
compile_module_artifact_from_source(source_path, source).expect("module compiles");
let chunk = artifact.type_schema_init_chunk.expect("schema initializer");
assert_eq!(chunk.source_file.as_deref(), Some("<test>/wrapped.harn"));
assert!(chunk
.constants
.iter()
.any(|constant| matches!(constant, Constant::String(value) if value == "External")));
}
}