harn_cli/commands/
precompile.rs1use harn_vm::bytecode_cache::{CACHE_EXTENSION, MODULE_CACHE_EXTENSION};
21use 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, &source, &program)?;
191 let entry_key = harn_vm::bytecode_cache::CacheKey::from_source(source_path, &source);
192
193 let entry_dest = output_path(source_path, source_root, out_root, CACHE_EXTENSION)?;
194 harn_vm::bytecode_cache::store_at(&entry_dest, &entry_key, &artifacts.entry_chunk)
195 .map_err(|e| format!("write {}: {e}", entry_dest.display()))?;
196
197 if let Some(module_artifact) = &artifacts.module_artifact {
198 let module_source = harn_vm::module_source::ModuleSource::from_text(source.as_str());
199 let module_key = harn_vm::bytecode_cache::CacheKey::from_module_source(&module_source);
200 let module_dest = output_path(source_path, source_root, out_root, MODULE_CACHE_EXTENSION)?;
201 harn_vm::bytecode_cache::store_module_at(&module_dest, &module_key, module_artifact)
202 .map_err(|e| format!("write {}: {e}", module_dest.display()))?;
203 }
204
205 Ok(entry_dest)
206}
207
208fn compile_artifacts(
215 source_path: &Path,
216 source: &str,
217 program: &[harn_parser::SNode],
218) -> Result<PrecompileArtifacts, String> {
219 let imported_enum_candidates = crate::imported_enum_candidates_for_source(source_path, source);
220 let entry_chunk =
221 crate::compiler_with_imported_enum_candidates(imported_enum_candidates.iter().cloned())
222 .compile(program)
223 .map_err(|e| format!("compile error: {e}"))?;
224 let module_artifact =
225 harn_vm::module_artifact::compile_module_artifact_from_source_with_imported_enums(
226 source_path,
227 source,
228 imported_enum_candidates,
229 )
230 .map_err(|e| format!("module compile error: {e}"))
231 .ok();
232 Ok(PrecompileArtifacts {
233 entry_chunk,
234 module_artifact,
235 })
236}
237
238fn output_path(
242 source_path: &Path,
243 source_root: Option<&Path>,
244 out_root: Option<&Path>,
245 extension: &str,
246) -> Result<PathBuf, String> {
247 let stem = source_path
248 .file_stem()
249 .ok_or_else(|| format!("source has no file stem: {}", source_path.display()))?;
250 let Some(out_root) = out_root else {
251 let parent = source_path.parent().unwrap_or_else(|| Path::new(""));
252 let mut adjacent = parent.join(stem);
253 adjacent.set_extension(extension);
254 return Ok(adjacent);
255 };
256 let relative = match source_root {
257 Some(root) => {
258 let canonical = source_path
259 .canonicalize()
260 .unwrap_or_else(|_| source_path.to_path_buf());
261 canonical
262 .strip_prefix(root)
263 .map(Path::to_path_buf)
264 .unwrap_or_else(|_| {
265 PathBuf::from(source_path.file_name().unwrap_or(source_path.as_os_str()))
266 })
267 }
268 None => PathBuf::from(
269 source_path
270 .file_name()
271 .ok_or_else(|| format!("source has no file name: {}", source_path.display()))?,
272 ),
273 };
274 let mut dest = out_root.join(&relative);
275 dest.set_extension(extension);
276 Ok(dest)
277}