harn_cli/commands/
precompile.rs1use std::path::{Path, PathBuf};
22
23use harn_parser::DiagnosticSeverity;
24use harn_vm::module_artifact::ModuleArtifact;
25
26use crate::cli::PrecompileArgs;
27use crate::command_error;
28use crate::commands::collect_harn_files;
29use crate::dispatch;
30use crate::env_guard::ScopedEnvVar;
31use crate::parse_source_file;
32use crate::typecheck_imports::checker_with_resolved_imports;
33
34pub const PRECOMPILE_BIN_ENV: &str = "HARN_CLI_SELF_EXE";
38
39const PRECOMPILE_OUT_ENV: &str = "HARN_PRECOMPILE_OUT";
43const PRECOMPILE_KEEP_GOING_ENV: &str = "HARN_PRECOMPILE_KEEP_GOING";
44const PRECOMPILE_QUIET_ENV: &str = "HARN_PRECOMPILE_QUIET";
45pub const PRECOMPILE_INNER_ENV: &str = "HARN_PRECOMPILE_INNER";
46
47pub async fn run(args: PrecompileArgs) {
48 if std::env::var(PRECOMPILE_INNER_ENV).as_deref() == Ok("1") {
49 run_inner_compile(args);
50 return;
51 }
52
53 let exe = std::env::current_exe().unwrap_or_else(|error| {
54 command_error(&format!("failed to resolve current executable: {error}"))
55 });
56 let exe_str = exe.to_string_lossy().into_owned();
57 let _bin = ScopedEnvVar::set(PRECOMPILE_BIN_ENV, &exe_str);
58 let _out = args
59 .out
60 .as_ref()
61 .map(|p| ScopedEnvVar::set(PRECOMPILE_OUT_ENV, &p.to_string_lossy()));
62 let _keep = if args.keep_going {
63 Some(ScopedEnvVar::set(PRECOMPILE_KEEP_GOING_ENV, "1"))
64 } else {
65 None
66 };
67 let _quiet = if args.quiet {
68 Some(ScopedEnvVar::set(PRECOMPILE_QUIET_ENV, "1"))
69 } else {
70 None
71 };
72
73 let argv = vec![args.target.to_string_lossy().into_owned()];
74 let exit = dispatch::dispatch_to_embedded_script_no_sandbox(
80 "precompile",
81 argv,
82 false,
83 )
84 .await;
85 if exit != 0 {
86 std::process::exit(exit);
87 }
88}
89
90#[derive(Default)]
92struct Stats {
93 compiled: usize,
94 failed: usize,
95}
96
97struct PrecompileArtifacts {
101 entry_chunk: harn_vm::Chunk,
102 module_artifact: Option<ModuleArtifact>,
103}
104
105pub fn run_inner_compile(args: PrecompileArgs) {
108 let target = args.target.clone();
109 if !target.exists() {
110 command_error(&format!("target does not exist: {}", target.display()));
111 }
112
113 let (sources, source_root) = if target.is_dir() {
114 let mut files = Vec::new();
115 collect_harn_files(&target, &mut files);
116 files.sort();
117 files.dedup();
118 let root = target.canonicalize().unwrap_or_else(|_| target.clone());
119 (files, Some(root))
120 } else {
121 (vec![target.clone()], None)
122 };
123
124 if sources.is_empty() {
125 command_error(&format!("no .harn files found under {}", target.display()));
126 }
127
128 let mut stats = Stats::default();
129 for source in &sources {
130 let result = precompile_one(source, source_root.as_deref(), args.out.as_deref());
131 match result {
132 Ok(out_path) => {
133 stats.compiled += 1;
134 if !args.quiet {
135 println!("{} -> {}", source.display(), out_path.display());
136 }
137 }
138 Err(err) => {
139 stats.failed += 1;
140 eprintln!("{}: {err}", source.display());
141 if !args.keep_going {
142 break;
143 }
144 }
145 }
146 }
147
148 if !args.quiet {
149 eprintln!(
150 "precompile: {} succeeded, {} failed",
151 stats.compiled, stats.failed
152 );
153 }
154 if stats.failed > 0 {
155 std::process::exit(1);
156 }
157}
158
159fn precompile_one(
160 source_path: &Path,
161 source_root: Option<&Path>,
162 out_root: Option<&Path>,
163) -> Result<PathBuf, String> {
164 let source = std::fs::read_to_string(source_path).map_err(|e| format!("read: {e}"))?;
165 let path_str = source_path.to_string_lossy();
166
167 let (parsed_source, program) = parse_source_file(&path_str);
168 debug_assert_eq!(parsed_source, source);
169
170 let checker = checker_with_resolved_imports(harn_parser::TypeChecker::new(), source_path);
173
174 let mut had_type_error = false;
175 let mut messages = String::new();
176 for diag in checker.check_with_source(&program, &source) {
177 let rendered = harn_parser::diagnostic::render_type_diagnostic(&source, &path_str, &diag);
178 if matches!(diag.severity, DiagnosticSeverity::Error) {
179 had_type_error = true;
180 }
181 messages.push_str(&rendered);
182 }
183 if had_type_error {
184 return Err(format!("type errors:\n{messages}"));
185 }
186 if !messages.is_empty() {
187 eprint!("{messages}");
188 }
189
190 let artifacts = compile_artifacts(source_path, &program)?;
191 let key = harn_vm::bytecode_cache::CacheKey::from_source(source_path, &source);
192
193 let entry_dest = output_path(
194 source_path,
195 source_root,
196 out_root,
197 harn_vm::bytecode_cache::CACHE_EXTENSION,
198 )?;
199 harn_vm::bytecode_cache::store_at(&entry_dest, &key, &artifacts.entry_chunk)
200 .map_err(|e| format!("write {}: {e}", entry_dest.display()))?;
201
202 if let Some(module_artifact) = &artifacts.module_artifact {
203 let module_dest = output_path(
204 source_path,
205 source_root,
206 out_root,
207 harn_vm::bytecode_cache::MODULE_CACHE_EXTENSION,
208 )?;
209 harn_vm::bytecode_cache::store_module_at(&module_dest, &key, module_artifact)
210 .map_err(|e| format!("write {}: {e}", module_dest.display()))?;
211 }
212
213 Ok(entry_dest)
214}
215
216fn compile_artifacts(
223 source_path: &Path,
224 program: &[harn_parser::SNode],
225) -> Result<PrecompileArtifacts, String> {
226 let entry_chunk = harn_vm::Compiler::new()
227 .compile(program)
228 .map_err(|e| format!("compile error: {e}"))?;
229 let module_artifact = harn_vm::module_artifact::compile_module_artifact(
230 program,
231 Some(source_path.display().to_string()),
232 )
233 .map_err(|e| format!("module compile error: {e}"))
234 .ok();
235 Ok(PrecompileArtifacts {
236 entry_chunk,
237 module_artifact,
238 })
239}
240
241fn output_path(
245 source_path: &Path,
246 source_root: Option<&Path>,
247 out_root: Option<&Path>,
248 extension: &str,
249) -> Result<PathBuf, String> {
250 let stem = source_path
251 .file_stem()
252 .ok_or_else(|| format!("source has no file stem: {}", source_path.display()))?;
253 let Some(out_root) = out_root else {
254 let parent = source_path.parent().unwrap_or_else(|| Path::new(""));
255 let mut adjacent = parent.join(stem);
256 adjacent.set_extension(extension);
257 return Ok(adjacent);
258 };
259 let relative = match source_root {
260 Some(root) => {
261 let canonical = source_path
262 .canonicalize()
263 .unwrap_or_else(|_| source_path.to_path_buf());
264 canonical
265 .strip_prefix(root)
266 .map(Path::to_path_buf)
267 .unwrap_or_else(|_| {
268 PathBuf::from(source_path.file_name().unwrap_or(source_path.as_os_str()))
269 })
270 }
271 None => PathBuf::from(
272 source_path
273 .file_name()
274 .ok_or_else(|| format!("source has no file name: {}", source_path.display()))?,
275 ),
276 };
277 let mut dest = out_root.join(&relative);
278 dest.set_extension(extension);
279 Ok(dest)
280}