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