Skip to main content

harn_cli/commands/
precompile.rs

1//! Implementation of the `harn precompile` subcommand.
2//!
3//! Walks a file or directory tree, compiles every `.harn` file into a
4//! `.harnbc` artifact, and either writes it adjacent to the source or
5//! mirrors the directory layout under `--out`. The on-disk artifact is
6//! the same format the runtime cache writes, so a shipped `.harnbc`
7//! beside its source elides parse+compile in the runtime loader.
8
9use std::path::{Path, PathBuf};
10
11use harn_parser::DiagnosticSeverity;
12use harn_vm::module_artifact::ModuleArtifact;
13
14use crate::cli::PrecompileArgs;
15use crate::command_error;
16use crate::commands::collect_harn_files;
17use crate::parse_source_file;
18
19/// Outcome aggregated across all sources walked in one invocation.
20#[derive(Default)]
21struct Stats {
22    compiled: usize,
23    failed: usize,
24}
25
26/// One file can be both an executable entry pipeline AND an imported
27/// module. Precompile emits both so the runtime loader hits whichever
28/// path the user takes.
29struct PrecompileArtifacts {
30    entry_chunk: harn_vm::Chunk,
31    module_artifact: Option<ModuleArtifact>,
32}
33
34pub fn run(args: PrecompileArgs) {
35    let target = args.target.clone();
36    if !target.exists() {
37        command_error(&format!("target does not exist: {}", target.display()));
38    }
39
40    let (sources, source_root) = if target.is_dir() {
41        let mut files = Vec::new();
42        collect_harn_files(&target, &mut files);
43        files.sort();
44        files.dedup();
45        let root = target.canonicalize().unwrap_or_else(|_| target.clone());
46        (files, Some(root))
47    } else {
48        (vec![target.clone()], None)
49    };
50
51    if sources.is_empty() {
52        command_error(&format!("no .harn files found under {}", target.display()));
53    }
54
55    let mut stats = Stats::default();
56    for source in &sources {
57        let result = precompile_one(source, source_root.as_deref(), args.out.as_deref());
58        match result {
59            Ok(out_path) => {
60                stats.compiled += 1;
61                if !args.quiet {
62                    println!("{} -> {}", source.display(), out_path.display());
63                }
64            }
65            Err(err) => {
66                stats.failed += 1;
67                eprintln!("{}: {err}", source.display());
68                if !args.keep_going {
69                    break;
70                }
71            }
72        }
73    }
74
75    if !args.quiet {
76        eprintln!(
77            "precompile: {} succeeded, {} failed",
78            stats.compiled, stats.failed
79        );
80    }
81    if stats.failed > 0 {
82        std::process::exit(1);
83    }
84}
85
86fn precompile_one(
87    source_path: &Path,
88    source_root: Option<&Path>,
89    out_root: Option<&Path>,
90) -> Result<PathBuf, String> {
91    let source = std::fs::read_to_string(source_path).map_err(|e| format!("read: {e}"))?;
92    let path_str = source_path.to_string_lossy();
93
94    let (parsed_source, program) = parse_source_file(&path_str);
95    debug_assert_eq!(parsed_source, source);
96
97    let mut had_type_error = false;
98    let mut messages = String::new();
99    for diag in harn_parser::TypeChecker::new().check_with_source(&program, &source) {
100        let rendered = harn_parser::diagnostic::render_type_diagnostic(&source, &path_str, &diag);
101        if matches!(diag.severity, DiagnosticSeverity::Error) {
102            had_type_error = true;
103        }
104        messages.push_str(&rendered);
105    }
106    if had_type_error {
107        return Err(format!("type errors:\n{messages}"));
108    }
109    if !messages.is_empty() {
110        eprint!("{messages}");
111    }
112
113    let artifacts = compile_artifacts(source_path, &program)?;
114    let key = harn_vm::bytecode_cache::CacheKey::from_source(source_path, &source);
115
116    let entry_dest = output_path(
117        source_path,
118        source_root,
119        out_root,
120        harn_vm::bytecode_cache::CACHE_EXTENSION,
121    )?;
122    harn_vm::bytecode_cache::store_at(&entry_dest, &key, &artifacts.entry_chunk)
123        .map_err(|e| format!("write {}: {e}", entry_dest.display()))?;
124
125    if let Some(module_artifact) = &artifacts.module_artifact {
126        let module_dest = output_path(
127            source_path,
128            source_root,
129            out_root,
130            harn_vm::bytecode_cache::MODULE_CACHE_EXTENSION,
131        )?;
132        harn_vm::bytecode_cache::store_module_at(&module_dest, &key, module_artifact)
133            .map_err(|e| format!("write {}: {e}", module_dest.display()))?;
134    }
135
136    Ok(entry_dest)
137}
138
139/// Compile both the entry-chunk view and the module-artifact view of the
140/// same source. A `.harn` file with a `pipeline default { ... }` block is
141/// callable as both an entry and an importable module; one without is
142/// importable but produces an entry chunk that just returns `nil`. We
143/// emit both artifacts unconditionally so the runtime loader hits the
144/// cache regardless of how the user invokes the file.
145fn compile_artifacts(
146    source_path: &Path,
147    program: &[harn_parser::SNode],
148) -> Result<PrecompileArtifacts, String> {
149    let entry_chunk = harn_vm::Compiler::new()
150        .compile(program)
151        .map_err(|e| format!("compile error: {e}"))?;
152    let module_artifact = harn_vm::module_artifact::compile_module_artifact(
153        program,
154        Some(source_path.display().to_string()),
155    )
156    .map_err(|e| format!("module compile error: {e}"))
157    .ok();
158    Ok(PrecompileArtifacts {
159        entry_chunk,
160        module_artifact,
161    })
162}
163
164/// Map a source path under (optional) `source_root` to its destination
165/// under (optional) `out_root` with the given file extension. When no
166/// `out_root` is given the artifact lands adjacent to the source.
167fn output_path(
168    source_path: &Path,
169    source_root: Option<&Path>,
170    out_root: Option<&Path>,
171    extension: &str,
172) -> Result<PathBuf, String> {
173    let stem = source_path
174        .file_stem()
175        .ok_or_else(|| format!("source has no file stem: {}", source_path.display()))?;
176    let Some(out_root) = out_root else {
177        let parent = source_path.parent().unwrap_or_else(|| Path::new(""));
178        let mut adjacent = parent.join(stem);
179        adjacent.set_extension(extension);
180        return Ok(adjacent);
181    };
182    let relative = match source_root {
183        Some(root) => {
184            let canonical = source_path
185                .canonicalize()
186                .unwrap_or_else(|_| source_path.to_path_buf());
187            canonical
188                .strip_prefix(root)
189                .map(Path::to_path_buf)
190                .unwrap_or_else(|_| {
191                    PathBuf::from(source_path.file_name().unwrap_or(source_path.as_os_str()))
192                })
193        }
194        None => PathBuf::from(
195            source_path
196                .file_name()
197                .ok_or_else(|| format!("source has no file name: {}", source_path.display()))?,
198        ),
199    };
200    let mut dest = out_root.join(&relative);
201    dest.set_extension(extension);
202    Ok(dest)
203}