harn_cli/commands/
precompile.rs1use std::path::{Path, PathBuf};
26
27use harn_parser::DiagnosticSeverity;
28use harn_vm::module_artifact::ModuleArtifact;
29
30use crate::cli::PrecompileArgs;
31use crate::command_error;
32use crate::commands::collect_harn_files;
33use crate::dispatch;
34use crate::env_guard::ScopedEnvVar;
35use crate::parse_source_file;
36
37pub const PRECOMPILE_BIN_ENV: &str = "HARN_CLI_SELF_EXE";
41
42const PRECOMPILE_OUT_ENV: &str = "HARN_PRECOMPILE_OUT";
46const PRECOMPILE_KEEP_GOING_ENV: &str = "HARN_PRECOMPILE_KEEP_GOING";
47const PRECOMPILE_QUIET_ENV: &str = "HARN_PRECOMPILE_QUIET";
48
49pub async fn run(args: PrecompileArgs) {
50 if std::env::var("HARN_CLI_IMPL").as_deref() == Ok("rust") {
51 run_legacy(args);
52 return;
53 }
54
55 let exe = std::env::current_exe().unwrap_or_else(|error| {
56 command_error(&format!("failed to resolve current executable: {error}"))
57 });
58 let exe_str = exe.to_string_lossy().into_owned();
59 let _bin = ScopedEnvVar::set(PRECOMPILE_BIN_ENV, &exe_str);
60 let _out = args
61 .out
62 .as_ref()
63 .map(|p| ScopedEnvVar::set(PRECOMPILE_OUT_ENV, &p.to_string_lossy()));
64 let _keep = if args.keep_going {
65 Some(ScopedEnvVar::set(PRECOMPILE_KEEP_GOING_ENV, "1"))
66 } else {
67 None
68 };
69 let _quiet = if args.quiet {
70 Some(ScopedEnvVar::set(PRECOMPILE_QUIET_ENV, "1"))
71 } else {
72 None
73 };
74
75 let argv = vec![args.target.to_string_lossy().into_owned()];
76 let exit = dispatch::dispatch_to_embedded_script_no_sandbox(
82 "precompile",
83 argv,
84 false,
85 )
86 .await;
87 if exit != 0 {
88 std::process::exit(exit);
89 }
90}
91
92#[derive(Default)]
94struct Stats {
95 compiled: usize,
96 failed: usize,
97}
98
99struct PrecompileArtifacts {
103 entry_chunk: harn_vm::Chunk,
104 module_artifact: Option<ModuleArtifact>,
105}
106
107pub fn run_legacy(args: PrecompileArgs) {
112 let target = args.target.clone();
113 if !target.exists() {
114 command_error(&format!("target does not exist: {}", target.display()));
115 }
116
117 let (sources, source_root) = if target.is_dir() {
118 let mut files = Vec::new();
119 collect_harn_files(&target, &mut files);
120 files.sort();
121 files.dedup();
122 let root = target.canonicalize().unwrap_or_else(|_| target.clone());
123 (files, Some(root))
124 } else {
125 (vec![target.clone()], None)
126 };
127
128 if sources.is_empty() {
129 command_error(&format!("no .harn files found under {}", target.display()));
130 }
131
132 let mut stats = Stats::default();
133 for source in &sources {
134 let result = precompile_one(source, source_root.as_deref(), args.out.as_deref());
135 match result {
136 Ok(out_path) => {
137 stats.compiled += 1;
138 if !args.quiet {
139 println!("{} -> {}", source.display(), out_path.display());
140 }
141 }
142 Err(err) => {
143 stats.failed += 1;
144 eprintln!("{}: {err}", source.display());
145 if !args.keep_going {
146 break;
147 }
148 }
149 }
150 }
151
152 if !args.quiet {
153 eprintln!(
154 "precompile: {} succeeded, {} failed",
155 stats.compiled, stats.failed
156 );
157 }
158 if stats.failed > 0 {
159 std::process::exit(1);
160 }
161}
162
163fn precompile_one(
164 source_path: &Path,
165 source_root: Option<&Path>,
166 out_root: Option<&Path>,
167) -> Result<PathBuf, String> {
168 let source = std::fs::read_to_string(source_path).map_err(|e| format!("read: {e}"))?;
169 let path_str = source_path.to_string_lossy();
170
171 let (parsed_source, program) = parse_source_file(&path_str);
172 debug_assert_eq!(parsed_source, source);
173
174 let mut had_type_error = false;
175 let mut messages = String::new();
176 for diag in harn_parser::TypeChecker::new().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}