1#![feature(rustc_private)]
2use annotate_snippets::{Level, Renderer};
3use clap::Parser;
4use colored::Colorize;
5use hax_types::cli_options::*;
6use hax_types::driver_api::*;
7use hax_types::engine_api::*;
8use is_terminal::IsTerminal;
9use serde_jsonlines::BufReadExt;
10use std::collections::HashMap;
11use std::fs;
12use std::io::BufRead;
13use std::io::Write;
14use std::path::PathBuf;
15use std::process;
16
17mod engine_debug_webapp;
18use hax_frontend_exporter::id_table;
19
20fn toolchain() -> Option<&'static str> {
24 let current_rustc_version = version_check::triple()
25 .map(|(_, channel, date)| format!("{channel}-{date}"))
26 .unwrap_or("unknown".into());
27 if env!("HAX_RUSTC_VERSION") != current_rustc_version {
28 const TOOLCHAIN: &str = env!("HAX_TOOLCHAIN");
29 which::which("rustup").ok().unwrap_or_else(|| {
31 println!("Error: {} was not found, but toolchain {} is required while the current toolchain is {}\n\nExiting.", "rustup".bold(), TOOLCHAIN.bold(), current_rustc_version.bold());
32 std::process::exit(1)
33 });
34 rustup_toolchain::install(TOOLCHAIN).unwrap();
36 Some(TOOLCHAIN)
38 } else {
39 None
40 }
41}
42
43pub fn get_args(subcommand: &str) -> Vec<String> {
47 let mut args: Vec<_> = std::env::args().collect();
48 if args.get(1) == Some(&subcommand.to_string()) {
49 args = args.into_iter().skip(1).collect();
51 }
52 args
53}
54
55const RUST_LOG_STYLE: &str = "RUST_LOG_STYLE";
63fn rust_log_style() -> String {
64 std::env::var(RUST_LOG_STYLE).unwrap_or_else(|_| {
65 if std::io::stderr().is_terminal() {
66 "always".to_string()
67 } else {
68 "never".to_string()
69 }
70 })
71}
72
73const RUSTFLAGS: &str = "RUSTFLAGS";
76fn rustflags() -> String {
77 let rustflags = std::env::var(RUSTFLAGS).unwrap_or("".into());
78 [rustflags, "--cfg hax".into()].join(" ")
79}
80
81const ENGINE_BINARY_NAME: &str = "hax-engine";
82const ENGINE_BINARY_NOT_FOUND: &str = "The binary [hax-engine] was not found in your [PATH].";
83
84#[allow(unused_variables, unreachable_code)]
90fn find_hax_engine(message_format: MessageFormat) -> process::Command {
91 use which::which;
92
93 std::env::var("HAX_ENGINE_BINARY")
94 .ok()
95 .map(process::Command::new)
96 .or_else(|| which(ENGINE_BINARY_NAME).ok().map(process::Command::new))
97 .or_else(|| {
98 which("node").ok().and_then(|_| {
99 if let Ok(true) = inquire::Confirm::new(&format!(
100 "{} Should I try to download it from GitHub?",
101 ENGINE_BINARY_NOT_FOUND,
102 ))
103 .with_default(true)
104 .prompt()
105 {
106 let cmd = process::Command::new("node");
107 let engine_js_path: String =
108 panic!("TODO: Downloading from GitHub is not supported yet.");
109 cmd.arg(engine_js_path);
110 Some(cmd)
111 } else {
112 None
113 }
114 })
115 })
116 .unwrap_or_else(|| {
117 fn is_opam_setup_correctly() -> bool {
118 std::env::var("OPAM_SWITCH_PREFIX").is_ok()
119 }
120 HaxMessage::EngineNotFound {
121 is_opam_setup_correctly: is_opam_setup_correctly(),
122 }
123 .report(message_format, None);
124 std::process::exit(2);
125 })
126}
127
128const RUST_ENGINE_BINARY_NAME: &str = "hax-rust-engine";
129const RUST_ENGINE_BINARY_NOT_FOUND: &str =
130 "The binary [hax-rust-engine] was not found in your [PATH].";
131
132#[allow(unused_variables, unreachable_code)]
133fn find_rust_hax_engine(message_format: MessageFormat) -> process::Command {
134 use which::which;
135
136 std::env::var("HAX_RUST_ENGINE_BINARY")
137 .ok()
138 .map(process::Command::new)
139 .or_else(|| {
140 which(RUST_ENGINE_BINARY_NAME)
141 .ok()
142 .map(process::Command::new)
143 })
144 .expect(RUST_ENGINE_BINARY_NOT_FOUND)
145}
146
147use hax_types::diagnostics::message::HaxMessage;
148use hax_types::diagnostics::report::ReportCtx;
149
150#[extension_traits::extension(trait ExtHaxMessage)]
151impl HaxMessage {
152 fn report(self, message_format: MessageFormat, rctx: Option<&mut ReportCtx>) {
153 match message_format {
154 MessageFormat::Json => println!("{}", serde_json::to_string(&self).unwrap()),
155 MessageFormat::Human => self.report_styled(rctx),
156 }
157 }
158 fn report_styled(self, rctx: Option<&mut ReportCtx>) {
159 let renderer = Renderer::styled();
160 match self {
161 Self::Diagnostic {
162 diagnostic,
163 working_dir,
164 } => {
165 let mut _rctx = None;
166 let rctx = rctx.unwrap_or_else(|| _rctx.get_or_insert(ReportCtx::default()));
167 diagnostic.with_message(rctx, &working_dir, Level::Error, |msg| {
168 eprintln!("{}", renderer.render(msg))
169 });
170 }
171 Self::EngineNotFound {
172 is_opam_setup_correctly,
173 } => {
174 use colored::Colorize;
175 let message = format!("hax: {}\n{}\n\n{} {}\n",
176 &ENGINE_BINARY_NOT_FOUND,
177 "Please make sure the engine is installed and is in PATH!",
178 "Hint: With OPAM, `eval $(opam env)` is necessary for OPAM binaries to be in PATH: make sure to run `eval $(opam env)` before running `cargo hax`.".bright_black(),
179 format!("(diagnostics: {})", if is_opam_setup_correctly { "opam seems okay ✓" } else {"opam seems not okay ❌"}).bright_black()
180 );
181 let message = Level::Error.title(&message);
182 eprintln!("{}", renderer.render(message))
183 }
184 Self::ProducedFile { mut path, wrote } => {
185 if let Ok(current_dir) = std::env::current_dir() {
187 if let Ok(relative) = path.strip_prefix(current_dir) {
188 path = PathBuf::from(".").join(relative).to_path_buf();
189 }
190 }
191 let title = if wrote {
192 format!("hax: wrote file {}", path.display())
193 } else {
194 format!("hax: unchanged file {}", path.display())
195 };
196 eprintln!("{}", renderer.render(Level::Info.title(&title)))
197 }
198 Self::HaxEngineFailure { exit_code } => {
199 let title = format!(
200 "hax: {} exited with non-zero code {}",
201 ENGINE_BINARY_NAME, exit_code,
202 );
203 eprintln!("{}", renderer.render(Level::Error.title(&title)));
204 }
205 Self::ProfilingData(data) => {
206 fn format_with_dot(shift: u32, n: u64) -> String {
207 let factor = 10u64.pow(shift);
208 format!("{}.{}", n / factor, n % factor)
209 }
210 let title = format!(
211 "hax[profiling]: {}: {}ms, memory={}, {} item{}{}",
212 data.context,
213 format_with_dot(6, data.time_ns),
214 data.memory,
215 data.quantity,
216 if data.quantity > 1 { "s" } else { "" },
217 if data.errored {
218 " (note: this failed!)"
219 } else {
220 ""
221 }
222 );
223 eprintln!("{}", renderer.render(Level::Info.title(&title)));
224 }
225 Self::Stats { errors_per_item } => {
226 let success_items = errors_per_item.iter().filter(|(_, n)| *n == 0).count();
227 let total = errors_per_item.len();
228 let title = format!(
229 "hax: {}/{} items were successfully translated ({}% success rate)",
230 success_items,
231 total,
232 (success_items * 100) / total
233 );
234 eprintln!("{}", renderer.render(Level::Info.title(&title)));
235 }
236 Self::CargoBuildFailure => {
237 let title =
238 "hax: running `cargo build` was not successful, continuing anyway.".to_string();
239 eprintln!("{}", renderer.render(Level::Warning.title(&title)));
240 }
241 Self::WarnExperimentalBackend { backend } => {
242 let title = format!(
243 "hax: Experimental backend \"{}\" is work in progress.",
244 backend
245 );
246 eprintln!("{}", renderer.render(Level::Warning.title(&title)));
247 }
248 }
249 }
250}
251
252fn run_engine(
254 haxmeta: HaxMeta<hax_frontend_exporter::ThirBody>,
255 id_table: id_table::Table,
256 working_dir: PathBuf,
257 manifest_dir: PathBuf,
258 backend: &BackendOptions<()>,
259 message_format: MessageFormat,
260) -> bool {
261 let engine_options = EngineOptions {
262 hax_version: haxmeta.hax_version,
263 backend: backend.clone(),
264 input: haxmeta.items,
265 impl_infos: haxmeta.impl_infos,
266 };
267 let mut hax_engine_command = if let Backend::Lean { .. }
268 | Backend::GenerateRustEngineNames { .. } =
269 &engine_options.backend.backend
270 {
271 find_rust_hax_engine(message_format)
272 } else {
273 find_hax_engine(message_format)
274 };
275 let mut engine_subprocess = hax_engine_command
276 .stdin(std::process::Stdio::piped())
277 .stdout(std::process::Stdio::piped())
278 .spawn()
279 .inspect_err(|e| {
280 if let std::io::ErrorKind::NotFound = e.kind() {
281 panic!(
282 "The binary [{}] was not found in your [PATH].",
283 ENGINE_BINARY_NAME
284 )
285 }
286 })
287 .unwrap();
288
289 let mut error = false;
290 let mut output = Output {
291 diagnostics: vec![],
292 files: vec![],
293 debug_json: None,
294 };
295 {
296 let mut rctx = hax_types::diagnostics::report::ReportCtx::default();
297 let mut stdin = std::io::BufWriter::new(
298 engine_subprocess
299 .stdin
300 .as_mut()
301 .expect("Could not write on stdin"),
302 );
303
304 macro_rules! send {
305 ($value:expr) => {
306 serde_json::to_writer(&mut stdin, $value).unwrap();
307 stdin.write_all(b"\n").unwrap();
308 stdin.flush().unwrap();
309 };
310 }
311
312 id_table::WithTable::run(id_table, engine_options, |with_table| {
313 send!(with_table);
314 });
315
316 let out_dir = backend.output_dir.clone().unwrap_or({
317 let relative_path: PathBuf = [
318 "proofs",
319 format!("{}", backend.backend).as_str(),
320 "extraction",
321 ]
322 .iter()
323 .collect();
324 manifest_dir.join(&relative_path)
325 });
326
327 let stdout = std::io::BufReader::new(engine_subprocess.stdout.take().unwrap());
328 let mut errors_per_item: HashMap<_, usize> = HashMap::new();
329 for msg in stdout.json_lines() {
330 let msg = msg.expect(
331 "Hax engine sent an invalid json value. \
332 This might be caused by debug messages on stdout, \
333 which is reserved for JSON communication with cargo-hax",
334 );
335 use protocol::*;
336 match msg {
337 FromEngine::Exit => break,
338 FromEngine::Diagnostic(diagnostic) => {
339 error = true;
340 if backend.dry_run {
341 output.diagnostics.push(diagnostic.clone())
342 }
343 if let Some(owner_id) = &diagnostic.owner_id {
344 *errors_per_item.entry(owner_id.clone()).or_default() += 1;
345 }
346 HaxMessage::Diagnostic {
347 diagnostic,
348 working_dir: working_dir.clone(),
349 }
350 .report(message_format, Some(&mut rctx));
351 }
352 FromEngine::File(file) => {
353 if backend.dry_run {
354 output.files.push(file)
355 } else {
356 let path = out_dir.join(&file.path);
357 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
358 let mut wrote = false;
359 if fs::read_to_string(&path).as_ref().ok() != Some(&file.contents) {
360 std::fs::write(&path, file.contents).unwrap();
361 wrote = true;
362 }
363 if let Some(mut sourcemap) = file.sourcemap.clone() {
364 sourcemap.sourcesContent = sourcemap
365 .sources
366 .iter()
367 .map(PathBuf::from)
368 .map(|path| {
369 if path.is_absolute() {
370 path
371 } else {
372 working_dir.join(path).to_path_buf()
373 }
374 })
375 .map(|path| fs::read_to_string(path).ok())
376 .collect();
377 let f = std::fs::File::create(path.with_file_name(format!(
378 "{}.map",
379 path.file_name().unwrap().to_string_lossy()
380 )))
381 .unwrap();
382 serde_json::to_writer(std::io::BufWriter::new(f), &sourcemap).unwrap()
383 }
384 HaxMessage::ProducedFile { path, wrote }.report(message_format, None)
385 }
386 }
387 FromEngine::DebugString(debug) => {
388 output.debug_json = Some(debug);
389 }
390 FromEngine::PrettyPrintDiagnostic(diag) => {
391 send!(&ToEngine::PrettyPrintedDiagnostic(format!("{}", diag)));
392 }
393 FromEngine::PrettyPrintRust(code) => {
394 let code = match syn::parse_file(&code) {
395 Ok(file) => match std::panic::catch_unwind(|| prettyplease::unparse(&file))
396 {
397 Ok(pp) => Ok(pp),
398 Err(err) => Err(format!("prettyplease panicked with: {:#?}", err)),
399 },
400 Err(err) => Err(format!("{}", err)),
401 };
402 send!(&ToEngine::PrettyPrintedRust(code));
403 }
404 FromEngine::ProfilingData(profiling_data) => {
405 HaxMessage::ProfilingData(profiling_data).report(message_format, None)
406 }
407 FromEngine::ItemProcessed(items) => {
408 for item in items {
409 errors_per_item.insert(item, 0);
410 }
411 }
412 FromEngine::Ping => {
413 send!(&ToEngine::Pong);
414 }
415 }
416 }
417 if backend.stats {
418 HaxMessage::Stats {
419 errors_per_item: errors_per_item.into_iter().collect(),
420 }
421 .report(message_format, None)
422 }
423 drop(stdin);
424 }
425
426 let exit_status = engine_subprocess.wait().unwrap();
427 if !exit_status.success() {
428 HaxMessage::HaxEngineFailure {
429 exit_code: exit_status.code().unwrap_or(-1),
430 }
431 .report(message_format, None);
432 std::process::exit(1);
433 }
434
435 if backend.dry_run {
436 serde_json::to_writer(std::io::BufWriter::new(std::io::stdout()), &output).unwrap()
437 }
438 if let Some(debug_json) = &output.debug_json {
439 use DebugEngineMode;
440 match &backend.debug_engine {
441 Some(DebugEngineMode::Interactive) => {
442 eprintln!("----------------------------------------------");
443 eprintln!("----------------------------------------------");
444 eprintln!("----------------------------------------------");
445 eprintln!("-- Engine debug mode. Press CTRL+C to exit. --");
446 eprintln!("----------------------------------------------");
447 eprintln!("----------------------------------------------");
448 eprintln!("----------------------------------------------");
449 engine_debug_webapp::run(|| debug_json.clone())
450 }
451 Some(DebugEngineMode::File(_file)) if !backend.dry_run => {
452 println!("{}", debug_json)
453 }
454 _ => (),
455 }
456 }
457
458 error
459}
460
461fn target_dir(suffix: &str) -> PathBuf {
463 let metadata = cargo_metadata::MetadataCommand::new().exec().unwrap();
464 let mut dir = metadata.target_directory;
465 dir.push(suffix);
466 dir.into()
467}
468
469fn get_hax_version() -> String {
475 let mut version = hax_types::HAX_VERSION.to_string();
476 if env!("HAX_GIT_IS_DIRTY") == "true" {
477 version += &std::env::current_exe()
478 .ok()
479 .and_then(|exe_path| std::fs::read(exe_path).ok())
480 .map(|contents| {
481 use std::hash::{DefaultHasher, Hash, Hasher};
482 let mut s = DefaultHasher::new();
483 contents.hash(&mut s);
484 format!("hash-exe-{}", s.finish())
485 })
486 .expect("Expect read path")
487 }
488
489 version
490}
491
492fn get_hax_rustc_driver_path() -> PathBuf {
498 std::env::current_exe()
499 .expect("Could not get the current executable path for `cargo-hax`.")
500 .parent().expect("The executable `cargo-hax` is supposed to be a file, which is supposed to have a parent folder.")
501 .join("driver-hax-frontend-exporter")
502}
503
504fn compute_haxmeta_files(options: &Options) -> (Vec<EmitHaxMetaMessage>, i32) {
508 let mut cmd = {
509 let mut cmd = process::Command::new("cargo");
510 if let Some(toolchain) = toolchain() {
511 cmd.env("RUSTUP_TOOLCHAIN", toolchain);
512 }
513 cmd.args(["check".into()].iter().chain(options.cargo_flags.iter()));
514 const COLOR_FLAG: &str = "--color";
515 let explicit_color_flag = options.cargo_flags.iter().any(|flag| flag == COLOR_FLAG);
516 if !explicit_color_flag && std::io::stderr().is_terminal() {
517 cmd.args([COLOR_FLAG, "always"]);
518 }
519 const MSG_FMT_FLAG: &str = "--message-format";
520 let explicit_msg_fmt_flag = options.cargo_flags.iter().any(|flag| flag == MSG_FMT_FLAG);
521 if !explicit_msg_fmt_flag && options.message_format == MessageFormat::Json {
522 cmd.args([MSG_FMT_FLAG, "json"]);
523 }
524 cmd.stderr(std::process::Stdio::piped());
525 if !options.no_custom_target_directory {
526 cmd.env("CARGO_TARGET_DIR", target_dir("hax"));
527 };
528 cmd.env("RUSTC_WORKSPACE_WRAPPER", get_hax_rustc_driver_path())
529 .env(RUST_LOG_STYLE, rust_log_style())
530 .env(RUSTFLAGS, rustflags())
531 .env("HAX_CARGO_CACHE_KEY", get_hax_version())
532 .env(
533 ENV_VAR_OPTIONS_FRONTEND,
534 serde_json::to_string(&options)
535 .expect("Options could not be converted to a JSON string"),
536 );
537 cmd
538 };
539
540 let mut child = cmd.spawn().unwrap();
541 let haxmeta_files = {
542 let mut haxmeta_files = vec![];
543 let stderr = child.stderr.take().unwrap();
544 let stderr = std::io::BufReader::new(stderr);
545 for line in std::io::BufReader::new(stderr).lines() {
546 if let Ok(line) = line {
547 if let Some(msg) = line.strip_prefix(HAX_DRIVER_STDERR_PREFIX) {
548 use HaxDriverMessage;
549 let msg = serde_json::from_str(msg).unwrap();
550 match msg {
551 HaxDriverMessage::EmitHaxMeta(data) => haxmeta_files.push(data),
552 }
553 } else {
554 eprintln!("{}", line);
555 }
556 }
557 }
558 haxmeta_files
559 };
560
561 let status = child
562 .wait()
563 .expect("`driver-hax-frontend-exporter`: could not start?");
564
565 let exit_code = if !status.success() {
566 HaxMessage::CargoBuildFailure.report(options.message_format, None);
567 status.code().unwrap_or(254)
568 } else {
569 0
570 };
571
572 (haxmeta_files, exit_code)
573}
574
575fn run_command(options: &Options, haxmeta_files: Vec<EmitHaxMetaMessage>) -> bool {
577 match options.command.clone() {
578 Command::JSON {
579 output_file,
580 kind,
581 include_extra,
582 use_ids,
583 ..
584 } => {
585 with_kind_type!(kind, <Body>|| {
586 for EmitHaxMetaMessage { path, .. } in haxmeta_files {
587 let (haxmeta, id_table): (HaxMeta<Body>, _) = HaxMeta::read(fs::File::open(&path).unwrap());
588 let dest = output_file.open_or_stdout();
589
590 (if include_extra {
591 let data = WithDefIds {
592 def_ids: haxmeta.def_ids,
593 impl_infos: haxmeta.impl_infos,
594 items: haxmeta.items,
595 comments: haxmeta.comments,
596 };
597 if use_ids {
598 id_table::WithTable::run(id_table, data, |with_table| {
599 serde_json::to_writer(dest, with_table)
600 })
601 } else {
602 serde_json::to_writer(dest, &data)
603 }
604 } else {
605 if use_ids {
606 id_table::WithTable::run(id_table, haxmeta.items, |with_table| {
607 serde_json::to_writer(dest, with_table)
608 })
609 } else {
610 serde_json::to_writer(dest, &haxmeta.items)
611 }
612 })
613 .unwrap()
614
615 }
616 });
617 false
618 }
619 Command::Backend(backend) => {
620 use Backend;
621 use hax_frontend_exporter::ThirBody as Body;
622
623 if matches!(backend.backend, Backend::Easycrypt | Backend::ProVerif(..)) {
624 HaxMessage::WarnExperimentalBackend {
625 backend: backend.backend.clone(),
626 }
627 .report(options.message_format, None);
628 }
629
630 let mut error = false;
631 for EmitHaxMetaMessage {
632 working_dir,
633 manifest_dir,
634 path,
635 } in haxmeta_files
636 {
637 let (haxmeta, id_table): (HaxMeta<Body>, _) =
638 HaxMeta::read(fs::File::open(&path).unwrap());
639
640 error = error
641 || run_engine(
642 haxmeta,
643 id_table,
644 working_dir,
645 manifest_dir,
646 &backend,
647 options.message_format,
648 );
649 }
650 error
651 }
652 }
653}
654
655fn main() {
656 let args: Vec<String> = get_args("hax");
657 let mut options = match &args[..] {
658 [_, kw] if kw == "__json" => serde_json::from_str(
659 &std::env::var(ENV_VAR_OPTIONS_FRONTEND).unwrap_or_else(|_| {
660 panic!(
661 "Cannot find environnement variable {}",
662 ENV_VAR_OPTIONS_FRONTEND
663 )
664 }),
665 )
666 .unwrap_or_else(|_| {
667 panic!(
668 "Invalid value for the environnement variable {}",
669 ENV_VAR_OPTIONS_FRONTEND
670 )
671 }),
672 _ => Options::parse_from(args.iter()),
673 };
674 options.normalize_paths();
675
676 let (haxmeta_files, exit_code) = compute_haxmeta_files(&options);
677 let error = run_command(&options, haxmeta_files);
678
679 std::process::exit(if exit_code == 0 && error {
680 1
681 } else {
682 exit_code
683 })
684}