From 62484d3890a62fc264c0643c56a9c20c7df46f5d Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Wed, 20 Apr 2022 13:50:00 -0400 Subject: [PATCH] Add `roc run` to run even if there are build errors. --- cli/src/build.rs | 46 ++++--------- cli/src/lib.rs | 126 +++++++++++++++++++++------------- cli/src/main.rs | 28 ++++++-- compiler/build/src/program.rs | 32 +++++++-- 4 files changed, 141 insertions(+), 91 deletions(-) diff --git a/cli/src/build.rs b/cli/src/build.rs index 0ccf118178..a898d7eb6f 100644 --- a/cli/src/build.rs +++ b/cli/src/build.rs @@ -1,7 +1,7 @@ use bumpalo::Bump; use roc_build::{ link::{link, rebuild_host, LinkType}, - program, + program::{self, Problems}, }; use roc_builtins::bitcode; use roc_load::LoadingProblem; @@ -21,25 +21,9 @@ fn report_timing(buf: &mut String, label: &str, duration: Duration) { )); } -pub enum BuildOutcome { - NoProblems, - OnlyWarnings, - Errors, -} - -impl BuildOutcome { - pub fn status_code(&self) -> i32 { - match self { - Self::NoProblems => 0, - Self::OnlyWarnings => 1, - Self::Errors => 2, - } - } -} - pub struct BuiltFile { pub binary_path: PathBuf, - pub outcome: BuildOutcome, + pub problems: Problems, pub total_time: Duration, } @@ -184,7 +168,7 @@ pub fn build_file<'a>( // This only needs to be mutable for report_problems. This can't be done // inside a nested scope without causing a borrow error! let mut loaded = loaded; - program::report_problems_monomorphized(&mut loaded); + let problems = program::report_problems_monomorphized(&mut loaded); let loaded = loaded; let code_gen_timing = program::gen_from_mono_module( @@ -243,7 +227,7 @@ pub fn build_file<'a>( // Step 2: link the precompiled host and compiled app let link_start = SystemTime::now(); - let outcome = if surgically_link { + let problems = if surgically_link { roc_linker::link_preprocessed_host(target, &host_input_path, app_o_file, &binary_path) .map_err(|err| { todo!( @@ -251,12 +235,12 @@ pub fn build_file<'a>( err ); })?; - BuildOutcome::NoProblems + problems } else if matches!(link_type, LinkType::None) { // Just copy the object file to the output folder. binary_path.set_extension(app_extension); std::fs::copy(app_o_file, &binary_path).unwrap(); - BuildOutcome::NoProblems + problems } else { let mut inputs = vec![ host_input_path.as_path().to_str().unwrap(), @@ -281,11 +265,15 @@ pub fn build_file<'a>( todo!("gracefully handle error after `ld` spawned"); })?; - // TODO change this to report whether there were errors or warnings! if exit_status.success() { - BuildOutcome::NoProblems + problems } else { - BuildOutcome::Errors + let mut problems = problems; + + // Add an error for `ld` failing + problems.errors += 1; + + problems } }; let linking_time = link_start.elapsed().unwrap(); @@ -298,7 +286,7 @@ pub fn build_file<'a>( Ok(BuiltFile { binary_path, - outcome, + problems, total_time, }) } @@ -350,10 +338,6 @@ fn spawn_rebuild_thread( } let rebuild_host_end = rebuild_host_start.elapsed().unwrap(); - if !precompiled { - println!("Done!"); - } - rebuild_host_end.as_millis() }) } @@ -364,7 +348,7 @@ pub fn check_file( src_dir: PathBuf, roc_file_path: PathBuf, emit_timings: bool, -) -> Result { +) -> Result { let compilation_start = SystemTime::now(); // only used for generating errors. We don't do code generation, so hardcoding should be fine diff --git a/cli/src/lib.rs b/cli/src/lib.rs index e64aeb9d63..6a503b80b5 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -1,7 +1,7 @@ #[macro_use] extern crate const_format; -use build::{BuildOutcome, BuiltFile}; +use build::BuiltFile; use bumpalo::Bump; use clap::{App, AppSettings, Arg, ArgMatches}; use roc_build::link::LinkType; @@ -24,6 +24,7 @@ mod format; pub use format::format; pub const CMD_BUILD: &str = "build"; +pub const CMD_RUN: &str = "run"; pub const CMD_REPL: &str = "repl"; pub const CMD_EDIT: &str = "edit"; pub const CMD_DOCS: &str = "docs"; @@ -142,6 +143,14 @@ pub fn build_app<'a>() -> App<'a> { .subcommand(App::new(CMD_REPL) .about("Launch the interactive Read Eval Print Loop (REPL)") ) + .subcommand(App::new(CMD_RUN) + .about("Run a .roc file even if it has build errors") + .arg( + Arg::new(ROC_FILE) + .about("The .roc file of an app to run") + .required(true), + ) + ) .subcommand(App::new(CMD_FORMAT) .about("Format a .roc file using standard Roc formatting") .arg( @@ -168,7 +177,7 @@ pub fn build_app<'a>() -> App<'a> { ) .arg( Arg::new(ROC_FILE) - .about("The .roc file of an app to run") + .about("The .roc file of an app to check") .required(true), ) ) @@ -273,6 +282,7 @@ pub fn docs(files: Vec) { pub enum BuildConfig { BuildOnly, BuildAndRun { roc_file_arg_index: usize }, + BuildAndRunIfNoErrors { roc_file_arg_index: usize }, } pub enum FormatMode { @@ -380,7 +390,7 @@ pub fn build(matches: &ArgMatches, config: BuildConfig) -> io::Result { match res_binary_path { Ok(BuiltFile { binary_path, - outcome, + problems, total_time, }) => { match config { @@ -401,50 +411,26 @@ pub fn build(matches: &ArgMatches, config: BuildConfig) -> io::Result { ); // Return a nonzero exit code if there were problems - Ok(outcome.status_code()) + Ok(problems.exit_code()) } - BuildAndRun { roc_file_arg_index } => { - let mut cmd = match triple.architecture { - Architecture::Wasm32 => { - // If possible, report the generated executable name relative to the current dir. - let generated_filename = binary_path - .strip_prefix(env::current_dir().unwrap()) - .unwrap_or(&binary_path); - - // No need to waste time freeing this memory, - // since the process is about to exit anyway. - std::mem::forget(arena); - - let args = std::env::args() - .skip(roc_file_arg_index) - .collect::>(); - - run_with_wasmer(generated_filename, &args); - return Ok(0); - } - _ => Command::new(&binary_path), - }; - - if let Architecture::Wasm32 = triple.architecture { - cmd.arg(binary_path); - } - - // Forward all the arguments after the .roc file argument - // to the new process. This way, you can do things like: - // - // roc app.roc foo bar baz - // - // ...and have it so that app.roc will receive only `foo`, - // `bar`, and `baz` as its arguments. - for (index, arg) in std::env::args().enumerate() { - if index > roc_file_arg_index { - cmd.arg(arg); - } - } - - match outcome { - BuildOutcome::Errors => Ok(outcome.status_code()), - _ => roc_run(cmd.current_dir(original_cwd)), + BuildAndRun { roc_file_arg_index } => roc_run( + &arena, + &original_cwd, + triple, + roc_file_arg_index, + &binary_path, + ), + BuildAndRunIfNoErrors { roc_file_arg_index } => { + if problems.errors == 0 { + roc_run( + &arena, + &original_cwd, + triple, + roc_file_arg_index, + &binary_path, + ) + } else { + Ok(problems.exit_code()) } } } @@ -461,11 +447,55 @@ pub fn build(matches: &ArgMatches, config: BuildConfig) -> io::Result { } #[cfg(target_family = "unix")] -fn roc_run(cmd: &mut Command) -> io::Result { +fn roc_run( + arena: &Bump, + cwd: &Path, + triple: Triple, + roc_file_arg_index: usize, + binary_path: &Path, +) -> io::Result { use std::os::unix::process::CommandExt; + let mut cmd = match triple.architecture { + Architecture::Wasm32 => { + // If possible, report the generated executable name relative to the current dir. + let generated_filename = binary_path + .strip_prefix(env::current_dir().unwrap()) + .unwrap_or(&binary_path); + + // No need to waste time freeing this memory, + // since the process is about to exit anyway. + std::mem::forget(arena); + + let args = std::env::args() + .skip(roc_file_arg_index) + .collect::>(); + + run_with_wasmer(generated_filename, &args); + return Ok(0); + } + _ => Command::new(&binary_path), + }; + + if let Architecture::Wasm32 = triple.architecture { + cmd.arg(binary_path); + } + + // Forward all the arguments after the .roc file argument + // to the new process. This way, you can do things like: + // + // roc app.roc foo bar baz + // + // ...and have it so that app.roc will receive only `foo`, + // `bar`, and `baz` as its arguments. + for (index, arg) in std::env::args().enumerate() { + if index > roc_file_arg_index { + cmd.arg(arg); + } + } + // This is much faster than spawning a subprocess if we're on a UNIX system! - let err = cmd.exec(); + let err = cmd.current_dir(cwd).exec(); // If exec actually returned, it was definitely an error! (Otherwise, // this process would have been replaced by the other one, and we'd diff --git a/cli/src/main.rs b/cli/src/main.rs index 6a5c984ef3..871e93af9a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,7 +1,8 @@ use roc_cli::build::check_file; use roc_cli::{ build_app, docs, format, BuildConfig, FormatMode, CMD_BUILD, CMD_CHECK, CMD_DOCS, CMD_EDIT, - CMD_FORMAT, CMD_REPL, CMD_VERSION, DIRECTORY_OR_FILES, FLAG_CHECK, FLAG_TIME, ROC_FILE, + CMD_FORMAT, CMD_REPL, CMD_RUN, CMD_VERSION, DIRECTORY_OR_FILES, FLAG_CHECK, FLAG_TIME, + ROC_FILE, }; use roc_load::LoadingProblem; use std::fs::{self, FileType}; @@ -27,7 +28,10 @@ fn main() -> io::Result<()> { Some(arg_index) => { let roc_file_arg_index = arg_index + 1; // Not sure why this +1 is necessary, but it is! - build(&matches, BuildConfig::BuildAndRun { roc_file_arg_index }) + build( + &matches, + BuildConfig::BuildAndRunIfNoErrors { roc_file_arg_index }, + ) } None => { @@ -37,6 +41,21 @@ fn main() -> io::Result<()> { } } } + Some((CMD_RUN, matches)) => { + match matches.index_of(ROC_FILE) { + Some(arg_index) => { + let roc_file_arg_index = arg_index + 1; // Not sure why this +1 is necessary, but it is! + + build(&matches, BuildConfig::BuildAndRun { roc_file_arg_index }) + } + + None => { + eprintln!("What .roc file do you want to run? Specify it at the end of the `roc run` command."); + + Ok(1) + } + } + } Some((CMD_BUILD, matches)) => Ok(build(matches, BuildConfig::BuildOnly)?), Some((CMD_CHECK, matches)) => { let arena = bumpalo::Bump::new(); @@ -47,10 +66,7 @@ fn main() -> io::Result<()> { let src_dir = roc_file_path.parent().unwrap().to_owned(); match check_file(&arena, src_dir, roc_file_path, emit_timings) { - Ok(number_of_errors) => { - let exit_code = if number_of_errors != 0 { 1 } else { 0 }; - Ok(exit_code) - } + Ok(problems) => Ok(problems.exit_code()), Err(LoadingProblem::FormattedReport(report)) => { print!("{}", report); diff --git a/compiler/build/src/program.rs b/compiler/build/src/program.rs index 518bc0c3e4..3d8ac5e456 100644 --- a/compiler/build/src/program.rs +++ b/compiler/build/src/program.rs @@ -28,7 +28,7 @@ const LLVM_VERSION: &str = "12"; // them after type checking (like Elm does) so we can complete the entire // `roc check` process without needing to monomorphize. /// Returns the number of problems reported. -pub fn report_problems_monomorphized(loaded: &mut MonomorphizedModule) -> usize { +pub fn report_problems_monomorphized(loaded: &mut MonomorphizedModule) -> Problems { report_problems_help( loaded.total_problems(), &loaded.sources, @@ -39,7 +39,7 @@ pub fn report_problems_monomorphized(loaded: &mut MonomorphizedModule) -> usize ) } -pub fn report_problems_typechecked(loaded: &mut LoadedModule) -> usize { +pub fn report_problems_typechecked(loaded: &mut LoadedModule) -> Problems { report_problems_help( loaded.total_problems(), &loaded.sources, @@ -50,6 +50,23 @@ pub fn report_problems_typechecked(loaded: &mut LoadedModule) -> usize { ) } +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct Problems { + pub errors: usize, + pub warnings: usize, +} + +impl Problems { + pub fn exit_code(&self) -> i32 { + // 0 means no problems, 1 means errors, 2 means warnings + if self.errors > 0 { + 1 + } else { + self.warnings.min(1) as i32 + } + } +} + fn report_problems_help( total_problems: usize, sources: &MutMap)>, @@ -57,7 +74,7 @@ fn report_problems_help( can_problems: &mut MutMap>, type_problems: &mut MutMap>, mono_problems: &mut MutMap>, -) -> usize { +) -> Problems { use roc_reporting::report::{ can_problem, mono_problem, type_problem, Report, RocDocAllocator, Severity::*, DEFAULT_PALETTE, @@ -144,13 +161,13 @@ fn report_problems_help( if errors.is_empty() { problems_reported = warnings.len(); - for warning in warnings { + for warning in warnings.iter() { println!("\n{}\n", warning); } } else { problems_reported = errors.len(); - for error in errors { + for error in errors.iter() { println!("\n{}\n", error); } } @@ -165,7 +182,10 @@ fn report_problems_help( println!("{}\u{001B}[0m\n", Report::horizontal_rule(&palette)); } - problems_reported + Problems { + errors: errors.len(), + warnings: warnings.len(), + } } #[cfg(not(feature = "llvm"))]