diff --git a/cli/src/format.rs b/cli/src/format.rs index fdc1d2bd6f..5504cf8e6d 100644 --- a/cli/src/format.rs +++ b/cli/src/format.rs @@ -25,7 +25,49 @@ use roc_parse::{ }; use roc_region::all::{Loc, Region}; +fn flatten_directories(files: std::vec::Vec) -> std::vec::Vec { + let mut to_flatten = files; + let mut files = vec![]; + + while let Some(path) = to_flatten.pop() { + if path.is_dir() { + match path.read_dir() { + Ok(directory) => { + for item in directory { + match item { + Ok(file) => { + let file_path = file.path(); + if file_path.is_dir() { + to_flatten.push(file_path); + } else if file_path.ends_with(".roc") { + files.push(file_path); + } + } + + Err(error) => internal_error!( + "There was an error while trying to read a file from a directory: {:?}", + error + ), + } + } + } + + Err(error) => internal_error!( + "There was an error while trying to read the contents of a directory: {:?}", + error + ), + } + } else { + files.push(path) + } + } + + files +} + pub fn format(files: std::vec::Vec, mode: FormatMode) -> Result<(), String> { + let files = flatten_directories(files); + for file in files { let arena = Bump::new(); diff --git a/cli/src/lib.rs b/cli/src/lib.rs index e3027927ea..b790552538 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -5,6 +5,7 @@ use build::{BuildOutcome, BuiltFile}; use bumpalo::Bump; use clap::{App, AppSettings, Arg, ArgMatches}; use roc_build::link::LinkType; +use roc_error_macros::user_error; use roc_load::file::LoadingProblem; use roc_mono::ir::OptLevel; use std::env; @@ -31,6 +32,7 @@ pub const CMD_FORMAT: &str = "format"; pub const FLAG_DEBUG: &str = "debug"; pub const FLAG_DEV: &str = "dev"; pub const FLAG_OPTIMIZE: &str = "optimize"; +pub const FLAG_OPT_SIZE: &str = "opt-size"; pub const FLAG_LIB: &str = "lib"; pub const FLAG_BACKEND: &str = "backend"; pub const FLAG_TIME: &str = "time"; @@ -61,6 +63,12 @@ pub fn build_app<'a>() -> App<'a> { .about("Optimize your compiled Roc program to run faster. (Optimization takes time to complete.)") .required(false), ) + .arg( + Arg::new(FLAG_OPT_SIZE) + .long(FLAG_OPT_SIZE) + .about("Optimize your compiled Roc program to have a small binary size. (Optimization takes time to complete.)") + .required(false), + ) .arg( Arg::new(FLAG_DEV) .long(FLAG_DEV) @@ -166,12 +174,18 @@ pub fn build_app<'a>() -> App<'a> { .requires(ROC_FILE) .required(false), ) - .arg( - Arg::new(FLAG_DEV) - .long(FLAG_DEV) - .about("Make compilation as fast as possible. (Runtime performance may suffer)") - .required(false), - ) + .arg( + Arg::new(FLAG_OPT_SIZE) + .long(FLAG_OPT_SIZE) + .about("Optimize your compiled Roc program to have a small binary size. (Optimization takes time to complete.)") + .required(false), + ) + .arg( + Arg::new(FLAG_DEV) + .long(FLAG_DEV) + .about("Make compilation as fast as possible. (Runtime performance may suffer)") + .required(false), + ) .arg( Arg::new(FLAG_DEBUG) .long(FLAG_DEBUG) @@ -272,12 +286,14 @@ pub fn build(matches: &ArgMatches, config: BuildConfig) -> io::Result { let original_cwd = std::env::current_dir()?; let opt_level = match ( matches.is_present(FLAG_OPTIMIZE), + matches.is_present(FLAG_OPT_SIZE), matches.is_present(FLAG_DEV), ) { - (true, false) => OptLevel::Optimize, - (true, true) => panic!("development cannot be optimized!"), - (false, true) => OptLevel::Development, - (false, false) => OptLevel::Normal, + (true, false, false) => OptLevel::Optimize, + (false, true, false) => OptLevel::Size, + (false, false, true) => OptLevel::Development, + (false, false, false) => OptLevel::Normal, + _ => user_error!("build can be only one of `--dev`, `--optimize`, or `--opt-size`"), }; let emit_debug_info = matches.is_present(FLAG_DEBUG); let emit_timings = matches.is_present(FLAG_TIME); diff --git a/cli/tests/cli_run.rs b/cli/tests/cli_run.rs index c91dd0294d..3d4824c33c 100644 --- a/cli/tests/cli_run.rs +++ b/cli/tests/cli_run.rs @@ -12,8 +12,8 @@ extern crate indoc; #[cfg(test)] mod cli_run { use cli_utils::helpers::{ - example_file, examples_dir, extract_valgrind_errors, fixture_file, known_bad_file, run_cmd, - run_roc, run_with_valgrind, Out, ValgrindError, ValgrindErrorXWhat, + example_file, examples_dir, extract_valgrind_errors, fixture_file, fixtures_dir, + known_bad_file, run_cmd, run_roc, run_with_valgrind, Out, ValgrindError, ValgrindErrorXWhat, }; use roc_test_utils::assert_multiline_str_eq; use serial_test::serial; @@ -904,6 +904,15 @@ mod cli_run { fn format_check_reformatting_needed() { check_format_check_as_expected(&fixture_file("format", "NotFormatted.roc"), false); } + + #[test] + fn format_check_folders() { + // This fails, because "NotFormatted.roc" is present in this folder + check_format_check_as_expected(&fixtures_dir("format"), false); + + // This doesn't fail, since only "Formatted.roc" is present in this folder + check_format_check_as_expected(&fixtures_dir("format/formatted_directory"), true); + } } #[allow(dead_code)] diff --git a/cli/tests/fixtures/format/formatted_directory/Formatted.roc b/cli/tests/fixtures/format/formatted_directory/Formatted.roc new file mode 100644 index 0000000000..b62c494e66 --- /dev/null +++ b/cli/tests/fixtures/format/formatted_directory/Formatted.roc @@ -0,0 +1,6 @@ +app "formatted" + packages { pf: "platform" } imports [] + provides [ main ] to pf + +main : Str +main = Dep1.value1 {} diff --git a/compiler/build/src/link.rs b/compiler/build/src/link.rs index 21ea8da01a..27ef1cc7ca 100644 --- a/compiler/build/src/link.rs +++ b/compiler/build/src/link.rs @@ -137,6 +137,8 @@ pub fn build_zig_host_native( if matches!(opt_level, OptLevel::Optimize) { command.args(&["-O", "ReleaseSafe"]); + } else if matches!(opt_level, OptLevel::Size) { + command.args(&["-O", "ReleaseSmall"]); } command.output().unwrap() } @@ -231,6 +233,8 @@ pub fn build_zig_host_native( ]); if matches!(opt_level, OptLevel::Optimize) { command.args(&["-O", "ReleaseSafe"]); + } else if matches!(opt_level, OptLevel::Size) { + command.args(&["-O", "ReleaseSmall"]); } command.output().unwrap() } @@ -282,6 +286,8 @@ pub fn build_zig_host_wasm32( ]); if matches!(opt_level, OptLevel::Optimize) { command.args(&["-O", "ReleaseSafe"]); + } else if matches!(opt_level, OptLevel::Size) { + command.args(&["-O", "ReleaseSmall"]); } command.output().unwrap() } @@ -317,7 +323,9 @@ pub fn build_c_host_native( command.args(&["-fPIC", "-c"]); } if matches!(opt_level, OptLevel::Optimize) { - command.arg("-O2"); + command.arg("-O3"); + } else if matches!(opt_level, OptLevel::Size) { + command.arg("-Os"); } command.output().unwrap() } @@ -351,6 +359,8 @@ pub fn build_swift_host_native( if matches!(opt_level, OptLevel::Optimize) { command.arg("-O"); + } else if matches!(opt_level, OptLevel::Size) { + command.arg("-Osize"); } command.output().unwrap() @@ -456,18 +466,18 @@ pub fn rebuild_host( } else if cargo_host_src.exists() { // Compile and link Cargo.toml, if it exists let cargo_dir = host_input_path.parent().unwrap(); - let cargo_out_dir = - cargo_dir - .join("target") - .join(if matches!(opt_level, OptLevel::Optimize) { - "release" - } else { - "debug" - }); + let cargo_out_dir = cargo_dir.join("target").join( + if matches!(opt_level, OptLevel::Optimize | OptLevel::Size) { + "release" + } else { + "debug" + }, + ); let mut command = Command::new("cargo"); command.arg("build").current_dir(cargo_dir); - if matches!(opt_level, OptLevel::Optimize) { + // Rust doesn't expose size without editing the cargo.toml. Instead just use release. + if matches!(opt_level, OptLevel::Optimize | OptLevel::Size) { command.arg("--release"); } let source_file = if shared_lib_path.is_some() { @@ -533,6 +543,8 @@ pub fn rebuild_host( ]); if matches!(opt_level, OptLevel::Optimize) { command.arg("-O"); + } else if matches!(opt_level, OptLevel::Size) { + command.arg("-C opt-level=s"); } let output = command.output().unwrap(); diff --git a/compiler/build/src/program.rs b/compiler/build/src/program.rs index 933e8f9e7a..53b74ae8b1 100644 --- a/compiler/build/src/program.rs +++ b/compiler/build/src/program.rs @@ -199,7 +199,7 @@ pub fn gen_from_mono_module( emit_debug_info: bool, ) -> CodeGenTiming { match opt_level { - OptLevel::Normal | OptLevel::Optimize => gen_from_mono_module_llvm( + OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => gen_from_mono_module_llvm( arena, loaded, roc_file_path, diff --git a/compiler/build/src/target.rs b/compiler/build/src/target.rs index 6064e58428..a49aff892b 100644 --- a/compiler/build/src/target.rs +++ b/compiler/build/src/target.rs @@ -114,6 +114,8 @@ pub fn target_machine( pub fn convert_opt_level(level: OptLevel) -> OptimizationLevel { match level { OptLevel::Development | OptLevel::Normal => OptimizationLevel::None, + // Default is O2/Os. If we want Oz, we have to explicitly turn of loop vectorization as well. + OptLevel::Size => OptimizationLevel::Default, OptLevel::Optimize => OptimizationLevel::Aggressive, } } diff --git a/compiler/gen_llvm/src/llvm/build.rs b/compiler/gen_llvm/src/llvm/build.rs index e81eea63f4..26f4816b84 100644 --- a/compiler/gen_llvm/src/llvm/build.rs +++ b/compiler/gen_llvm/src/llvm/build.rs @@ -657,35 +657,43 @@ pub fn construct_optimization_passes<'a>( OptLevel::Development | OptLevel::Normal => { pmb.set_optimization_level(OptimizationLevel::None); } + OptLevel::Size => { + pmb.set_optimization_level(OptimizationLevel::Default); + // TODO: For some usecase, like embedded, it is useful to expose this and tune it. + pmb.set_inliner_with_threshold(50); + } OptLevel::Optimize => { pmb.set_optimization_level(OptimizationLevel::Aggressive); // this threshold seems to do what we want pmb.set_inliner_with_threshold(275); - - // TODO figure out which of these actually help - - // function passes - - fpm.add_cfg_simplification_pass(); - mpm.add_cfg_simplification_pass(); - - fpm.add_jump_threading_pass(); - mpm.add_jump_threading_pass(); - - fpm.add_memcpy_optimize_pass(); // this one is very important - - fpm.add_licm_pass(); - - // turn invoke into call - mpm.add_prune_eh_pass(); - - // remove unused global values (often the `_wrapper` can be removed) - mpm.add_global_dce_pass(); - - mpm.add_function_inlining_pass(); } } + // Add optimization passes for Size and Optimize. + if matches!(opt_level, OptLevel::Size | OptLevel::Optimize) { + // TODO figure out which of these actually help + + // function passes + + fpm.add_cfg_simplification_pass(); + mpm.add_cfg_simplification_pass(); + + fpm.add_jump_threading_pass(); + mpm.add_jump_threading_pass(); + + fpm.add_memcpy_optimize_pass(); // this one is very important + + fpm.add_licm_pass(); + + // turn invoke into call + mpm.add_prune_eh_pass(); + + // remove unused global values (often the `_wrapper` can be removed) + mpm.add_global_dce_pass(); + + mpm.add_function_inlining_pass(); + } + pmb.populate_module_pass_manager(&mpm); pmb.populate_function_pass_manager(&fpm); diff --git a/compiler/mono/src/alias_analysis.rs b/compiler/mono/src/alias_analysis.rs index 810e3c3595..f25e3e3b08 100644 --- a/compiler/mono/src/alias_analysis.rs +++ b/compiler/mono/src/alias_analysis.rs @@ -245,7 +245,7 @@ where match opt_level { OptLevel::Development | OptLevel::Normal => morphic_lib::solve_trivial(program), - OptLevel::Optimize => morphic_lib::solve(program), + OptLevel::Optimize | OptLevel::Size => morphic_lib::solve(program), } } diff --git a/compiler/mono/src/ir.rs b/compiler/mono/src/ir.rs index 63dbbaa0b1..7bc4653330 100644 --- a/compiler/mono/src/ir.rs +++ b/compiler/mono/src/ir.rs @@ -90,6 +90,7 @@ macro_rules! return_on_layout_error_help { pub enum OptLevel { Development, Normal, + Size, Optimize, }