diff --git a/crates/cli/src/build.rs b/crates/cli/src/build.rs index cf9c8038f6..9033947879 100644 --- a/crates/cli/src/build.rs +++ b/crates/cli/src/build.rs @@ -35,6 +35,13 @@ pub struct BuiltFile { pub interns: Interns, } +pub enum BuildOrdering { + /// Run up through typechecking first; continue building iff that is successful. + BuildIfChecks, + /// Always build the Roc binary, even if there are type errors. + AlwaysBuild, +} + #[allow(clippy::too_many_arguments)] pub fn build_file<'a>( arena: &'a Bump, @@ -48,6 +55,7 @@ pub fn build_file<'a>( precompiled: bool, threading: Threading, wasm_dev_stack_bytes: Option, + order: BuildOrdering, ) -> Result> { let compilation_start = Instant::now(); let target_info = TargetInfo::from(target); @@ -55,12 +63,17 @@ pub fn build_file<'a>( // Step 1: compile the app and generate the .o file let subs_by_module = Default::default(); + let exec_mode = match order { + BuildOrdering::BuildIfChecks => ExecutionMode::ExecutableIfCheck, + BuildOrdering::AlwaysBuild => ExecutionMode::Executable, + }; + let load_config = LoadConfig { target_info, // TODO: expose this from CLI? render: RenderTarget::ColorTerminal, threading, - exec_mode: ExecutionMode::Executable, + exec_mode, }; let loaded = roc_load::load_and_monomorphize( arena, diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index b4e43f3474..1e49c65968 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -33,6 +33,8 @@ pub mod build; mod format; pub use format::format; +use crate::build::BuildOrdering; + const DEFAULT_ROC_FILENAME: &str = "main.roc"; pub const CMD_BUILD: &str = "build"; @@ -521,6 +523,10 @@ pub fn build( .and_then(|s| s.parse::().ok()) .map(|x| x * 1024); + let build_ordering = match config { + BuildAndRunIfNoErrors => BuildOrdering::BuildIfChecks, + _ => BuildOrdering::AlwaysBuild, + }; let res_binary_path = build_file( &arena, &triple, @@ -533,6 +539,7 @@ pub fn build( precompiled, threading, wasm_dev_stack_bytes, + build_ordering, ); match res_binary_path { diff --git a/crates/compiler/load_internal/src/file.rs b/crates/compiler/load_internal/src/file.rs index 751f8d6d83..cb5819a7dc 100644 --- a/crates/compiler/load_internal/src/file.rs +++ b/crates/compiler/load_internal/src/file.rs @@ -130,13 +130,15 @@ pub enum ExecutionMode { Test, Check, Executable, + /// Like [`ExecutionMode::Executable`], but stops in the presence of type errors. + ExecutableIfCheck, } impl ExecutionMode { fn goal_phase(&self) -> Phase { match self { ExecutionMode::Test | ExecutionMode::Executable => Phase::MakeSpecializations, - ExecutionMode::Check => Phase::SolveTypes, + ExecutionMode::Check | ExecutionMode::ExecutableIfCheck => Phase::SolveTypes, } } } @@ -168,6 +170,22 @@ struct ModuleCache<'a> { sources: MutMap, } +impl<'a> ModuleCache<'a> { + pub fn total_problems(&self) -> usize { + let mut total = 0; + + for problems in self.can_problems.values() { + total += problems.len(); + } + + for problems in self.type_problems.values() { + total += problems.len(); + } + + total + } +} + impl Default for ModuleCache<'_> { fn default() -> Self { let mut module_names = MutMap::default(); @@ -2379,7 +2397,12 @@ fn update<'a>( .extend(solved_module.aliases.keys().copied()); } - if is_host_exposed && state.goal_phase() == Phase::SolveTypes { + let finish_type_checking = is_host_exposed && + (state.goal_phase() == Phase::SolveTypes) + // If we're running in check-and-then-build mode, only exit now there are errors. + && (!matches!(state.exec_mode, ExecutionMode::ExecutableIfCheck) || state.module_cache.total_problems() > 0); + + if finish_type_checking { debug_assert!(work.is_empty()); debug_assert!(state.dependencies.solved_all()); @@ -2421,7 +2444,9 @@ fn update<'a>( }, ); - if state.goal_phase() > Phase::SolveTypes { + if state.goal_phase() > Phase::SolveTypes + || matches!(state.exec_mode, ExecutionMode::ExecutableIfCheck) + { let layout_cache = state .layout_caches .pop() @@ -2446,6 +2471,25 @@ fn update<'a>( state.timings.insert(module_id, module_timing); } + let work = if is_host_exposed + && matches!(state.exec_mode, ExecutionMode::ExecutableIfCheck) + { + debug_assert!( + work.is_empty(), + "work left over after host exposed is checked" + ); + + // Update the goal phase to target full codegen. + state.exec_mode = ExecutionMode::Executable; + + // Load the find + make specializations portion of the dependency graph. + state + .dependencies + .load_find_and_make_specializations_after_check() + } else { + work + }; + start_tasks(arena, &mut state, work, injector, worker_listeners)?; } @@ -2803,7 +2847,7 @@ fn finish_specialization( let entry_point = { match exec_mode { ExecutionMode::Test => EntryPoint::Test, - ExecutionMode::Executable => { + ExecutionMode::Executable | ExecutionMode::ExecutableIfCheck => { let path_to_platform = { use PlatformPath::*; let package_name = match platform_path { @@ -5000,7 +5044,9 @@ fn build_pending_specializations<'a>( // skip expectations if we're not going to run them match execution_mode { ExecutionMode::Test => { /* fall through */ } - ExecutionMode::Check | ExecutionMode::Executable => continue, + ExecutionMode::Check + | ExecutionMode::Executable + | ExecutionMode::ExecutableIfCheck => continue, } // mark this symbol as a top-level thunk before any other work on the procs @@ -5074,7 +5120,9 @@ fn build_pending_specializations<'a>( // skip expectations if we're not going to run them match execution_mode { ExecutionMode::Test => { /* fall through */ } - ExecutionMode::Check | ExecutionMode::Executable => continue, + ExecutionMode::Check + | ExecutionMode::Executable + | ExecutionMode::ExecutableIfCheck => continue, } // mark this symbol as a top-level thunk before any other work on the procs diff --git a/crates/compiler/load_internal/src/work.rs b/crates/compiler/load_internal/src/work.rs index 6c2226f6cb..23ff2e901a 100644 --- a/crates/compiler/load_internal/src/work.rs +++ b/crates/compiler/load_internal/src/work.rs @@ -166,11 +166,10 @@ impl<'a> Dependencies<'a> { } } - if goal_phase >= MakeSpecializations { - // Add make specialization dependents - self.make_specializations_dependents - .add_succ(module_id, dependencies.iter().map(|dep| *dep.as_inner())); - } + // Add "make specialization" dependents. Even if we're not targetting making + // specializations right now, we may re-enter to do so later. + self.make_specializations_dependents + .add_succ(module_id, dependencies.iter().map(|dep| *dep.as_inner())); // add dependencies for self // phase i + 1 of a file always depends on phase i being completed @@ -374,6 +373,80 @@ impl<'a> Dependencies<'a> { } } + /// Loads the dependency graph to find and make specializations, and returns the next jobs to + /// be run. + /// + /// This should be used when the compiler wants to build or run a Roc executable if and only if + /// previous stages succeed; in such cases we load the dependency graph dynamically. + pub fn load_find_and_make_specializations_after_check(&mut self) -> MutSet<(ModuleId, Phase)> { + let mut output = MutSet::default(); + + let mut make_specializations_dependents = MakeSpecializationsDependents::default(); + let default_make_specializations_dependents_len = make_specializations_dependents.0.len(); + std::mem::swap( + &mut self.make_specializations_dependents, + &mut make_specializations_dependents, + ); + + for (&module, info) in make_specializations_dependents.0.iter_mut() { + debug_assert!(self.status.get_mut(&Job::Step(module, Phase::FindSpecializations)).is_none(), "should only have targetted solving types, but there is already a goal to find specializations"); + debug_assert!(self.status.get_mut(&Job::Step(module, Phase::MakeSpecializations)).is_none(), "should only have targetted solving types, but there is already a goal to make specializations"); + debug_assert!( + module == ModuleId::DERIVED_GEN || info.succ.contains(&ModuleId::DERIVED_GEN), + "derived module not accounted for in {:?}", + (module, info) + ); + + let mut has_find_specialization_dep = false; + for &module_dep in info.succ.iter() { + // The modules in `succ` are the modules for which specializations should be made + // after the current one. But, their specializations should be found before the + // current one. + if module_dep != ModuleId::DERIVED_GEN { + // We never find specializations for DERIVED_GEN + self.add_dependency(module, module_dep, Phase::FindSpecializations); + has_find_specialization_dep = true; + } + + self.add_dependency(module_dep, module, Phase::MakeSpecializations); + self.add_dependency(ModuleId::DERIVED_GEN, module, Phase::MakeSpecializations); + + // `module_dep` can't make its specializations until the current module does. + info.has_pred = true; + } + + if module != ModuleId::DERIVED_GEN { + self.add_to_status_for_phase(module, Phase::FindSpecializations); + self.add_dependency_help( + module, + module, + Phase::MakeSpecializations, + Phase::FindSpecializations, + ); + } + self.add_to_status_for_phase(module, Phase::MakeSpecializations); + + if !has_find_specialization_dep && module != ModuleId::DERIVED_GEN { + // We don't depend on any other modules having their specializations found first, + // so start finding specializations from this module. + output.insert((module, Phase::FindSpecializations)); + } + } + + std::mem::swap( + &mut self.make_specializations_dependents, + &mut make_specializations_dependents, + ); + debug_assert_eq!( + make_specializations_dependents.0.len(), + default_make_specializations_dependents_len, + "more modules were added to the graph: {:?}", + make_specializations_dependents + ); + + output + } + /// Load the entire "make specializations" dependency graph and start from the top. pub fn reload_make_specialization_pass(&mut self) -> MutSet<(ModuleId, Phase)> { let mut output = MutSet::default();