use std::path::{Path, PathBuf};
use harn_parser::DiagnosticSeverity;
use harn_vm::bytecode_cache::{
CacheKey, PrecompiledInterface, CACHE_EXTENSION, INTERFACE_CACHE_EXTENSION,
MODULE_CACHE_EXTENSION,
};
use harn_vm::module_artifact::{ModuleArtifact, ModuleCompilationContext, ModuleProvenance};
use crate::package;
const GENERATED_SOURCE_WALK_DIRS: &[&str] = &[
".burin",
".build",
".claude",
".codex",
".git",
".next",
".svelte-kit",
".turbo",
".venv",
"build",
"coverage",
"dist",
"node_modules",
"target",
];
pub fn should_skip_recursive_source_dir(dir: &Path) -> bool {
let Some(name) = dir.file_name().and_then(|name| name.to_str()) else {
return false;
};
GENERATED_SOURCE_WALK_DIRS.contains(&name)
|| crate::path_policy::is_harn_internal_entry(
name,
crate::path_policy::PathEntryKind::Directory,
)
}
pub fn should_skip_recursive_source_file(file: &Path) -> bool {
let Some(name) = file.file_name().and_then(|name| name.to_str()) else {
return false;
};
name.starts_with(".harn-")
}
pub fn collect_harn_files(dir: &Path, out: &mut Vec<PathBuf>) {
if let Ok(entries) = std::fs::read_dir(dir) {
let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
entries.sort_by_key(|e| e.path());
for entry in entries {
let path = entry.path();
if path.is_dir() {
if should_skip_recursive_source_dir(&path) {
continue;
}
collect_harn_files(&path, out);
} else if should_skip_recursive_source_file(&path) {
continue;
} else if path.extension().is_some_and(|ext| ext == "harn") {
let skip_marker = path.with_extension("conformance-skip");
if skip_marker.exists() {
continue;
}
out.push(path);
}
}
}
}
#[derive(Clone, Copy)]
pub struct SourceCompilerAuthority {
trusted_host_dispatch: bool,
}
impl SourceCompilerAuthority {
pub fn for_source(path: &Path) -> Self {
Self {
trusted_host_dispatch: trusted_host_dispatch_for_source(path),
}
}
pub fn standalone() -> Self {
Self {
trusted_host_dispatch: false,
}
}
pub fn typechecker(self) -> harn_parser::TypeChecker {
harn_parser::TypeChecker::new().with_privileged_wire_builtins(self.trusted_host_dispatch)
}
pub fn module_provenance(self) -> ModuleProvenance {
if self.trusted_host_dispatch {
ModuleProvenance::TrustedHostDispatch
} else {
ModuleProvenance::User
}
}
pub fn compiler_with_imported_symbols(
self,
context: &ModuleCompilationContext,
) -> harn_vm::Compiler {
let compiler = if self.trusted_host_dispatch {
harn_vm::Compiler::new_trusted_host_dispatch()
} else {
harn_vm::Compiler::new()
};
context.configure_compiler(compiler)
}
pub fn compile_module_with_imported_symbols(
self,
source_path: &Path,
source: &str,
context: &ModuleCompilationContext,
) -> Result<ModuleArtifact, harn_vm::VmError> {
if self.trusted_host_dispatch {
harn_vm::module_artifact::compile_trusted_host_dispatch_module_artifact_from_source_with_context(
source_path,
source,
context,
)
} else {
harn_vm::module_artifact::compile_module_artifact_from_source_with_context(
source_path,
source,
context,
)
}
}
}
pub fn trusted_host_dispatch_for_source(path: &Path) -> bool {
let absolute = path
.canonicalize()
.unwrap_or_else(|_| std::env::current_dir().unwrap_or_default().join(path));
package::load_check_config(Some(&absolute)).trusted_host_dispatch
}
pub enum Outcome {
Compiled(PathBuf),
Reused(PathBuf),
}
impl Outcome {
pub fn destination(&self) -> &PathBuf {
match self {
Outcome::Compiled(path) | Outcome::Reused(path) => path,
}
}
}
pub struct Precompiled {
pub outcome: Outcome,
pub warnings: String,
}
pub fn precompile_source(
source_path: &Path,
source_root: Option<&Path>,
out_root: Option<&Path>,
) -> Result<Precompiled, String> {
let source = std::fs::read_to_string(source_path).map_err(|e| format!("read: {e}"))?;
let path_str = source_path.to_string_lossy();
harn_parser::install_builtin_manifest(harn_vm::stdlib::all_builtin_manifest());
let authority = SourceCompilerAuthority::for_source(source_path);
let interface_dest = output_path(
source_path,
source_root,
out_root,
INTERFACE_CACHE_EXTENSION,
)?;
let (keys, interface) = artifact_keys(
source_path,
&source,
source_root,
&authority,
&interface_dest,
)?;
let entry_dest = output_path(source_path, source_root, out_root, CACHE_EXTENSION)?;
let module_dest = output_path(source_path, source_root, out_root, MODULE_CACHE_EXTENSION)?;
if keys.match_artifacts_at(&entry_dest, &module_dest) {
interface
.store()
.map_err(|e| format!("write {}: {e}", interface_dest.display()))?;
return Ok(Precompiled {
outcome: Outcome::Reused(entry_dest),
warnings: String::new(),
});
}
let program = harn_parser::parse_source(&source).map_err(|e| format!("parse error: {e}"))?;
let graph = harn_modules::build(&[source_path.to_path_buf()]);
let checker = graph
.typecheck_import_config_for_file(source_path)
.apply_imports_to(authority.typechecker());
let mut had_type_error = false;
let mut warnings = String::new();
let reported = checker.check_with_source(&program, &source).into_iter();
for diag in reported.filter(|diag| diag.severity.reported_when_executing()) {
had_type_error |= matches!(diag.severity, DiagnosticSeverity::Error);
warnings.push_str(&harn_parser::diagnostic::render_type_diagnostic(
&source, &path_str, &diag,
));
}
if had_type_error {
return Err(format!("type errors:\n{warnings}"));
}
let entry_chunk = authority
.compiler_with_imported_symbols(interface.context())
.compile(&program)
.map_err(|e| format!("compile error: {e}"))?;
let module_artifact = authority
.compile_module_with_imported_symbols(source_path, &source, interface.context())
.ok();
interface
.store()
.map_err(|e| format!("write {}: {e}", interface_dest.display()))?;
harn_vm::bytecode_cache::store_at(&entry_dest, &keys.entry, &entry_chunk)
.map_err(|e| format!("write {}: {e}", entry_dest.display()))?;
if let Some(module_artifact) = &module_artifact {
harn_vm::bytecode_cache::store_module_at(&module_dest, &keys.module, module_artifact)
.map_err(|e| format!("write {}: {e}", module_dest.display()))?;
}
Ok(Precompiled {
outcome: Outcome::Compiled(entry_dest),
warnings,
})
}
#[derive(Default)]
pub struct TreeReport {
pub compiled: usize,
pub reused: usize,
pub failed: Vec<(PathBuf, String)>,
}
pub fn precompile_tree(root: &Path) -> TreeReport {
let mut sources = Vec::new();
collect_harn_files(root, &mut sources);
let root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let mut report = TreeReport::default();
for source in sources {
match precompile_source(&source, Some(&root), None) {
Ok(Precompiled {
outcome: Outcome::Compiled(_),
..
}) => report.compiled += 1,
Ok(Precompiled {
outcome: Outcome::Reused(_),
..
}) => report.reused += 1,
Err(error) => report.failed.push((source, error)),
}
}
report
}
struct ArtifactKeys {
entry: CacheKey,
module: CacheKey,
}
impl ArtifactKeys {
fn match_artifacts_at(&self, entry_dest: &Path, module_dest: &Path) -> bool {
harn_vm::bytecode_cache::entry_artifact_at_matches(entry_dest, &self.entry)
&& harn_vm::bytecode_cache::module_artifact_at_matches(module_dest, &self.module)
}
}
fn artifact_keys(
source_path: &Path,
source: &str,
source_root: Option<&Path>,
authority: &SourceCompilerAuthority,
interface_dest: &Path,
) -> Result<(ArtifactKeys, PrecompiledInterface), String> {
let mut interface =
harn_vm::bytecode_cache::prepare_precompiled_interface(interface_dest, source_path, source)
.map_err(|e| format!("prepare {}: {e}", interface_dest.display()))?;
if !interface.package_import_aliases().is_empty() {
let graph = harn_modules::build_with_source(source_path, source);
package::ensure_reachable_dependencies_materialized(source_path, &graph)
.map_err(|e| e.to_string())?;
interface = harn_vm::bytecode_cache::prepare_precompiled_interface(
interface_dest,
source_path,
source,
)
.map_err(|e| e.to_string())?;
}
let aliases =
package::declared_package_import_aliases(source_path, interface.package_import_aliases())
.map_err(|e| e.to_string())?;
let digest = package::reachable_dependency_lock_digest(source_path, &aliases)
.map_err(|e| e.to_string())?;
interface.set_package_authority(aliases, digest);
let entry = if source_root.is_some() {
CacheKey::from_relocatable_source(source_path, source)
} else {
CacheKey::from_source(source_path, source)
};
let module_source = harn_vm::module_source::ModuleSource::from_text(source);
let module = CacheKey::from_module_source(
&module_source,
interface.context(),
authority.module_provenance(),
);
Ok((ArtifactKeys { entry, module }, interface))
}
fn output_path(
source_path: &Path,
source_root: Option<&Path>,
out_root: Option<&Path>,
extension: &str,
) -> Result<PathBuf, String> {
let stem = source_path
.file_stem()
.ok_or_else(|| format!("source has no file stem: {}", source_path.display()))?;
let Some(out_root) = out_root else {
let parent = source_path.parent().unwrap_or_else(|| Path::new(""));
let mut adjacent = parent.join(stem);
adjacent.set_extension(extension);
return Ok(adjacent);
};
let relative = match source_root {
Some(root) => {
let canonical = source_path
.canonicalize()
.unwrap_or_else(|_| source_path.to_path_buf());
canonical
.strip_prefix(root)
.map(Path::to_path_buf)
.unwrap_or_else(|_| {
PathBuf::from(source_path.file_name().unwrap_or(source_path.as_os_str()))
})
}
None => PathBuf::from(
source_path
.file_name()
.ok_or_else(|| format!("source has no file name: {}", source_path.display()))?,
),
};
let mut dest = out_root.join(&relative);
dest.set_extension(extension);
Ok(dest)
}