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