harn_cli/commands/
precompile.rs1use 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#[derive(Default)]
21struct Stats {
22 compiled: usize,
23 failed: usize,
24}
25
26struct 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
139fn 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
164fn 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}