use crate::docs::ModuleDocumentation; use bumpalo::Bump; use crossbeam::channel::{bounded, Sender}; use crossbeam::deque::{Injector, Stealer, Worker}; use crossbeam::thread; use parking_lot::Mutex; use roc_builtins::roc::module_source; use roc_can::abilities::{AbilitiesStore, PendingAbilitiesStore, ResolvedImpl}; use roc_can::constraint::{Constraint as ConstraintSoa, Constraints}; use roc_can::expr::Declarations; use roc_can::expr::PendingDerives; use roc_can::module::{ canonicalize_module_defs, ExposedByModule, ExposedForModule, ExposedModuleTypes, Module, ResolvedImplementations, }; use roc_collections::{default_hasher, BumpMap, MutMap, MutSet, VecMap, VecSet}; use roc_constrain::module::constrain_module; use roc_debug_flags::dbg_do; #[cfg(debug_assertions)] use roc_debug_flags::{ ROC_PRINT_IR_AFTER_REFCOUNT, ROC_PRINT_IR_AFTER_RESET_REUSE, ROC_PRINT_IR_AFTER_SPECIALIZATION, ROC_PRINT_LOAD_LOG, }; use roc_derive::SharedDerivedModule; use roc_error_macros::internal_error; use roc_late_solve::{AbilitiesView, WorldAbilities}; use roc_module::ident::{Ident, ModuleName, QualifiedModuleName}; use roc_module::symbol::{ IdentIds, IdentIdsByModule, Interns, ModuleId, ModuleIds, PQModuleName, PackageModuleIds, PackageQualified, Symbol, }; use roc_mono::ir::{ CapturedSymbols, ExternalSpecializations, PartialProc, Proc, ProcLayout, Procs, ProcsBase, UpdateModeIds, }; use roc_mono::layout::{CapturesNiche, LambdaName, Layout, LayoutCache, LayoutProblem}; use roc_parse::ast::{self, Defs, ExtractSpaces, Spaced, StrLiteral, TypeAnnotation}; use roc_parse::header::{ExposedName, ImportsEntry, PackageEntry, PlatformHeader, To, TypedIdent}; use roc_parse::header::{HeaderFor, ModuleNameEnum, PackageName}; use roc_parse::ident::UppercaseIdent; use roc_parse::module::module_defs; use roc_parse::parser::{FileError, Parser, SyntaxError}; use roc_region::all::{LineInfo, Loc, Region}; use roc_reporting::report::RenderTarget; use roc_solve::module::{extract_module_owned_implementations, Solved, SolvedModule}; use roc_solve_problem::TypeError; use roc_target::TargetInfo; use roc_types::subs::{ExposedTypesStorageSubs, Subs, VarStore, Variable}; use roc_types::types::{Alias, AliasKind}; use std::collections::hash_map::Entry::{Occupied, Vacant}; use std::collections::HashMap; use std::env::current_dir; use std::io; use std::iter; use std::ops::ControlFlow; use std::path::{Path, PathBuf}; use std::str::from_utf8_unchecked; use std::sync::Arc; use std::{env, fs}; use crate::work::Dependencies; pub use crate::work::Phase; #[cfg(target_family = "wasm")] use crate::wasm_instant::{Duration, Instant}; #[cfg(not(target_family = "wasm"))] use std::time::{Duration, Instant}; /// Default name for the binary generated for an app, if an invalid one was specified. const DEFAULT_APP_OUTPUT_PATH: &str = "app"; /// Filename extension for normal Roc modules const ROC_FILE_EXTENSION: &str = "roc"; /// The . in between module names like Foo.Bar.Baz const MODULE_SEPARATOR: char = '.'; const EXPANDED_STACK_SIZE: usize = 8 * 1024 * 1024; const PRELUDE_TYPES: [(&str, Symbol); 33] = [ ("Num", Symbol::NUM_NUM), ("Int", Symbol::NUM_INT), ("Frac", Symbol::NUM_FRAC), ("Integer", Symbol::NUM_INTEGER), ("FloatingPoint", Symbol::NUM_FLOATINGPOINT), ("Binary32", Symbol::NUM_BINARY32), ("Binary64", Symbol::NUM_BINARY64), ("Signed128", Symbol::NUM_SIGNED128), ("Signed64", Symbol::NUM_SIGNED64), ("Signed32", Symbol::NUM_SIGNED32), ("Signed16", Symbol::NUM_SIGNED16), ("Signed8", Symbol::NUM_SIGNED8), ("Unsigned128", Symbol::NUM_UNSIGNED128), ("Unsigned64", Symbol::NUM_UNSIGNED64), ("Unsigned32", Symbol::NUM_UNSIGNED32), ("Unsigned16", Symbol::NUM_UNSIGNED16), ("Unsigned8", Symbol::NUM_UNSIGNED8), ("Natural", Symbol::NUM_NATURAL), ("Decimal", Symbol::NUM_DECIMAL), ("Nat", Symbol::NUM_NAT), ("I8", Symbol::NUM_I8), ("I16", Symbol::NUM_I16), ("I32", Symbol::NUM_I32), ("I64", Symbol::NUM_I64), ("I128", Symbol::NUM_I128), ("U8", Symbol::NUM_U8), ("U16", Symbol::NUM_U16), ("U32", Symbol::NUM_U32), ("U64", Symbol::NUM_U64), ("U128", Symbol::NUM_U128), ("F32", Symbol::NUM_F32), ("F64", Symbol::NUM_F64), ("Dec", Symbol::NUM_DEC), ]; macro_rules! log { ($($arg:tt)*) => (dbg_do!(ROC_PRINT_LOAD_LOG, println!($($arg)*))) } #[derive(Debug)] pub struct LoadConfig { pub target_info: TargetInfo, pub render: RenderTarget, pub threading: Threading, pub exec_mode: ExecutionMode, } #[derive(Debug, Clone, Copy)] 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 | ExecutionMode::ExecutableIfCheck => Phase::SolveTypes, } } } /// Struct storing various intermediate stages by their ModuleId #[derive(Debug)] struct ModuleCache<'a> { module_names: MutMap>, /// Phases headers: MutMap>, parsed: MutMap>, aliases: MutMap>, pending_abilities: MutMap, constrained: MutMap, typechecked: MutMap>, found_specializations: MutMap>, late_specializations: MutMap>, external_specializations_requested: MutMap>>, expectations: VecMap, /// Various information imports: MutMap>, top_level_thunks: MutMap>, documentation: MutMap, can_problems: MutMap>, type_problems: MutMap>, 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(); macro_rules! insert_builtins { ($($name:ident,)*) => {$( module_names.insert( ModuleId::$name, PQModuleName::Unqualified(ModuleName::from(ModuleName::$name)), ); )*} } insert_builtins! { RESULT, LIST, STR, DICT, SET, BOOL, NUM, BOX, ENCODE, DECODE, JSON, } Self { module_names, headers: Default::default(), parsed: Default::default(), aliases: Default::default(), pending_abilities: Default::default(), constrained: Default::default(), typechecked: Default::default(), found_specializations: Default::default(), late_specializations: Default::default(), external_specializations_requested: Default::default(), imports: Default::default(), top_level_thunks: Default::default(), documentation: Default::default(), can_problems: Default::default(), type_problems: Default::default(), sources: Default::default(), expectations: Default::default(), } } } type SharedIdentIdsByModule = Arc>; fn start_phase<'a>( module_id: ModuleId, phase: Phase, arena: &'a Bump, state: &mut State<'a>, ) -> Vec> { // we blindly assume all dependencies are met use crate::work::PrepareStartPhase::*; match state.dependencies.prepare_start_phase(module_id, phase) { Continue => { // fall through } Done => { // no more work to do return vec![]; } Recurse(new) => { return new .into_iter() .flat_map(|(module_id, phase)| start_phase(module_id, phase, arena, state)) .collect() } } let task = { match phase { Phase::LoadHeader => { let opt_dep_name = state.module_cache.module_names.get(&module_id); match opt_dep_name { None => { panic!("Module {:?} is not in module_cache.module_names", module_id) } Some(dep_name) => { let module_name = dep_name.clone(); BuildTask::LoadModule { module_name, // Provide mutexes of ModuleIds and IdentIds by module, // so other modules can populate them as they load. module_ids: Arc::clone(&state.arc_modules), shorthands: Arc::clone(&state.arc_shorthands), ident_ids_by_module: Arc::clone(&state.ident_ids_by_module), } } } } Phase::Parse => { // parse the file let header = state.module_cache.headers.remove(&module_id).unwrap(); BuildTask::Parse { header } } Phase::CanonicalizeAndConstrain => { // canonicalize the file let parsed = state.module_cache.parsed.remove(&module_id).unwrap(); let deps_by_name = &parsed.deps_by_name; let num_deps = deps_by_name.len(); let mut dep_idents: IdentIdsByModule = IdentIds::exposed_builtins(num_deps); let State { ident_ids_by_module, .. } = &state; { let ident_ids_by_module = (*ident_ids_by_module).lock(); // Populate dep_idents with each of their IdentIds, // which we'll need during canonicalization to translate // identifier strings into IdentIds, which we need to build Symbols. // We only include the modules we care about (the ones we import). // // At the end of this loop, dep_idents contains all the information to // resolve a symbol from another module: if it's in here, that means // we have both imported the module and the ident was exported by that mdoule. for dep_id in deps_by_name.values() { // We already verified that these are all present, // so unwrapping should always succeed here. let idents = ident_ids_by_module.get(dep_id).unwrap(); dep_idents.insert(*dep_id, idents.clone()); } } // Clone the module_ids we'll need for canonicalization. // This should be small, and cloning it should be quick. // We release the lock as soon as we're done cloning, so we don't have // to lock the global module_ids while canonicalizing any given module. let qualified_module_ids = Arc::clone(&state.arc_modules); let qualified_module_ids = { (*qualified_module_ids).lock().clone() }; let module_ids = qualified_module_ids.into_module_ids(); let exposed_symbols = state .exposed_symbols_by_module .get(&module_id) .expect("Could not find listener ID in exposed_symbols_by_module") .clone(); let mut aliases = MutMap::default(); let mut abilities_store = PendingAbilitiesStore::default(); for imported in parsed.imported_modules.keys() { match state.module_cache.aliases.get(imported) { None => unreachable!( r"imported module {:?} did not register its aliases, so {:?} cannot use them", imported, parsed.module_id, ), Some(new) => { aliases.extend(new.iter().filter_map(|(s, (exposed, a))| { // only pass this on if it's exposed, or the alias is a transitive import if *exposed || s.module_id() != *imported { Some((*s, a.clone())) } else { None } })); } } match state.module_cache.pending_abilities.get(imported) { None => unreachable!( r"imported module {:?} did not register its abilities, so {:?} cannot use them", imported, parsed.module_id, ), Some(import_store) => { let exposed_symbols = state .exposed_symbols_by_module .get(imported) .unwrap_or_else(|| { internal_error!( "Could not find exposed symbols of imported {:?}", imported ) }); abilities_store .union(import_store.closure_from_imported(exposed_symbols)); } } } let skip_constraint_gen = { // Give this its own scope to make sure that the Guard from the lock() is dropped // immediately after contains_key returns state.cached_subs.lock().contains_key(&module_id) }; BuildTask::CanonicalizeAndConstrain { parsed, dep_idents, exposed_symbols, module_ids, aliases, abilities_store, skip_constraint_gen, } } Phase::SolveTypes => { let constrained = state.module_cache.constrained.remove(&module_id).unwrap(); let ConstrainedModule { module, ident_ids, module_timing, constraints, constraint, var_store, imported_modules, declarations, dep_idents, pending_derives, .. } = constrained; let derived_module = SharedDerivedModule::clone(&state.derived_module); BuildTask::solve_module( module, ident_ids, module_timing, constraints, constraint, pending_derives, var_store, imported_modules, &state.exposed_types, dep_idents, declarations, state.cached_subs.clone(), derived_module, ) } Phase::FindSpecializations => { let typechecked = state.module_cache.typechecked.remove(&module_id).unwrap(); let TypeCheckedModule { layout_cache, module_id, module_timing, solved_subs, decls, ident_ids, abilities_store, } = typechecked; let mut imported_module_thunks = bumpalo::collections::Vec::new_in(arena); if let Some(imports) = state.module_cache.imports.get(&module_id) { for imported in imports.iter() { imported_module_thunks.extend( state.module_cache.top_level_thunks[imported] .iter() .copied(), ); } } let derived_module = SharedDerivedModule::clone(&state.derived_module); BuildTask::BuildPendingSpecializations { layout_cache, execution_mode: state.exec_mode, module_id, module_timing, solved_subs, imported_module_thunks: imported_module_thunks.into_bump_slice(), decls, ident_ids, exposed_to_host: state.exposed_to_host.clone(), abilities_store, // TODO: awful, how can we get rid of the clone? exposed_by_module: state.exposed_types.clone(), derived_module, } } Phase::MakeSpecializations => { let mut specializations_we_must_make = state .module_cache .external_specializations_requested .remove(&module_id) .unwrap_or_default(); if module_id == ModuleId::DERIVED_GEN { // The derived gen module must also fulfill also specializations asked of the // derived synth module. let derived_synth_specializations = state .module_cache .external_specializations_requested .remove(&ModuleId::DERIVED_SYNTH) .unwrap_or_default(); specializations_we_must_make.extend(derived_synth_specializations) } let (mut ident_ids, mut subs, mut procs_base, layout_cache, mut module_timing) = if state.make_specializations_pass.current_pass() == 1 && module_id == ModuleId::DERIVED_GEN { // This is the first time the derived module is introduced into the load // graph. It has no abilities of its own or anything, just generate fresh // information for it. ( IdentIds::default(), Subs::default(), ProcsBase::default(), LayoutCache::new(state.target_info), ModuleTiming::new(Instant::now()), ) } else if state.make_specializations_pass.current_pass() == 1 { let found_specializations = state .module_cache .found_specializations .remove(&module_id) .unwrap(); let FoundSpecializationsModule { ident_ids, subs, procs_base, layout_cache, module_timing, abilities_store, } = found_specializations; let our_exposed_types = state .exposed_types .get(&module_id) .unwrap_or_else(|| { internal_error!("Exposed types for {:?} missing", module_id) }) .clone(); // Add our abilities to the world. state.world_abilities.insert( module_id, abilities_store, our_exposed_types.exposed_types_storage_subs, ); (ident_ids, subs, procs_base, layout_cache, module_timing) } else { let LateSpecializationsModule { ident_ids, subs, module_timing, layout_cache, procs_base, } = state .module_cache .late_specializations .remove(&module_id) .unwrap(); (ident_ids, subs, procs_base, layout_cache, module_timing) }; if module_id == ModuleId::DERIVED_GEN { load_derived_partial_procs( module_id, arena, &mut subs, &mut ident_ids, &state.derived_module, &mut module_timing, state.target_info, &state.exposed_types, &mut procs_base, &mut state.world_abilities, ); } let derived_module = SharedDerivedModule::clone(&state.derived_module); BuildTask::MakeSpecializations { module_id, ident_ids, subs, procs_base, layout_cache, specializations_we_must_make, module_timing, world_abilities: state.world_abilities.clone_ref(), // TODO: awful, how can we get rid of the clone? exposed_by_module: state.exposed_types.clone(), derived_module, } } } }; vec![task] } #[derive(Debug)] pub struct LoadedModule { pub module_id: ModuleId, pub interns: Interns, pub solved: Solved, pub can_problems: MutMap>, pub type_problems: MutMap>, pub declarations_by_id: MutMap, pub exposed_to_host: MutMap, pub dep_idents: IdentIdsByModule, pub exposed_aliases: MutMap, pub exposed_values: Vec, pub exposed_types_storage: ExposedTypesStorageSubs, pub resolved_implementations: ResolvedImplementations, pub sources: MutMap)>, pub timings: MutMap, pub documentation: MutMap, pub abilities_store: AbilitiesStore, } impl LoadedModule { 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 } pub fn exposed_values_str(&self) -> Vec<&str> { self.exposed_values .iter() .map(|symbol| symbol.as_str(&self.interns)) .collect() } } #[derive(Debug)] pub enum BuildProblem<'a> { FileNotFound(&'a Path), } #[derive(Debug)] struct ModuleHeader<'a> { module_id: ModuleId, module_name: ModuleNameEnum<'a>, module_path: PathBuf, is_root_module: bool, exposed_ident_ids: IdentIds, deps_by_name: MutMap, ModuleId>, packages: MutMap<&'a str, PackageName<'a>>, imported_modules: MutMap, package_qualified_imported_modules: MutSet>, exposes: Vec, exposed_imports: MutMap, parse_state: roc_parse::state::State<'a>, header_for: HeaderFor<'a>, symbols_from_requires: Vec<(Loc, Loc>)>, module_timing: ModuleTiming, } #[derive(Debug)] struct ConstrainedModule { module: Module, declarations: Declarations, imported_modules: MutMap, constraints: Constraints, constraint: ConstraintSoa, ident_ids: IdentIds, var_store: VarStore, dep_idents: IdentIdsByModule, module_timing: ModuleTiming, // Rather than adding pending derives as constraints, hand them directly to solve because they // must be solved at the end of a module. pending_derives: PendingDerives, } #[derive(Debug)] pub struct TypeCheckedModule<'a> { pub module_id: ModuleId, pub layout_cache: LayoutCache<'a>, pub module_timing: ModuleTiming, pub solved_subs: Solved, pub decls: Declarations, pub ident_ids: IdentIds, pub abilities_store: AbilitiesStore, } #[derive(Debug)] struct FoundSpecializationsModule<'a> { ident_ids: IdentIds, layout_cache: LayoutCache<'a>, procs_base: ProcsBase<'a>, subs: Subs, module_timing: ModuleTiming, abilities_store: AbilitiesStore, } #[derive(Debug)] struct LateSpecializationsModule<'a> { ident_ids: IdentIds, subs: Subs, module_timing: ModuleTiming, layout_cache: LayoutCache<'a>, procs_base: ProcsBase<'a>, } #[derive(Debug, Default)] pub struct ToplevelExpects { pub pure: VecMap, pub fx: VecMap, } #[derive(Debug)] pub struct MonomorphizedModule<'a> { pub module_id: ModuleId, pub interns: Interns, pub subs: Subs, pub output_path: Box, pub can_problems: MutMap>, pub type_problems: MutMap>, pub procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, pub toplevel_expects: ToplevelExpects, pub entry_point: EntryPoint<'a>, pub exposed_to_host: ExposedToHost, pub sources: MutMap)>, pub timings: MutMap, pub expectations: VecMap, } #[derive(Debug)] pub enum EntryPoint<'a> { Executable { symbol: Symbol, layout: ProcLayout<'a>, platform_path: Box, }, Test, } #[derive(Debug)] pub struct Expectations { pub subs: roc_types::subs::Subs, pub path: PathBuf, pub expectations: VecMap>, pub ident_ids: IdentIds, } #[derive(Clone, Debug, Default)] pub struct ExposedToHost { /// usually `mainForHost` pub values: MutMap, /// exposed closure types, typically `Fx` pub closure_types: Vec, } impl<'a> MonomorphizedModule<'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 } } #[derive(Debug)] struct ParsedModule<'a> { module_id: ModuleId, module_path: PathBuf, src: &'a str, module_timing: ModuleTiming, deps_by_name: MutMap, ModuleId>, imported_modules: MutMap, exposed_ident_ids: IdentIds, exposed_imports: MutMap, parsed_defs: Defs<'a>, module_name: ModuleNameEnum<'a>, symbols_from_requires: Vec<(Loc, Loc>)>, header_for: HeaderFor<'a>, } type LocExpects = VecMap>; /// A message sent out _from_ a worker thread, /// representing a result of work done, or a request for further work #[derive(Debug)] enum Msg<'a> { Many(Vec>), Header(ModuleHeader<'a>), Parsed(ParsedModule<'a>), CanonicalizedAndConstrained(CanAndCon), SolvedTypes { module_id: ModuleId, ident_ids: IdentIds, solved_module: SolvedModule, solved_subs: Solved, decls: Declarations, dep_idents: IdentIdsByModule, module_timing: ModuleTiming, abilities_store: AbilitiesStore, loc_expects: LocExpects, }, FinishedAllTypeChecking { solved_subs: Solved, exposed_vars_by_symbol: Vec<(Symbol, Variable)>, exposed_aliases_by_symbol: MutMap, exposed_types_storage: ExposedTypesStorageSubs, resolved_implementations: ResolvedImplementations, dep_idents: IdentIdsByModule, documentation: MutMap, abilities_store: AbilitiesStore, }, FoundSpecializations { module_id: ModuleId, ident_ids: IdentIds, layout_cache: LayoutCache<'a>, procs_base: ProcsBase<'a>, solved_subs: Solved, module_timing: ModuleTiming, abilities_store: AbilitiesStore, toplevel_expects: ToplevelExpects, }, MadeSpecializations { module_id: ModuleId, ident_ids: IdentIds, layout_cache: LayoutCache<'a>, external_specializations_requested: BumpMap>, procs_base: ProcsBase<'a>, procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, update_mode_ids: UpdateModeIds, module_timing: ModuleTiming, subs: Subs, }, /// The task is to only typecheck AND monomorphize modules /// all modules are now monomorphized, we are done FinishedAllSpecialization { subs: Subs, exposed_to_host: ExposedToHost, }, FailedToParse(FileError<'a, SyntaxError<'a>>), FailedToReadFile { filename: PathBuf, error: io::ErrorKind, }, } #[derive(Debug)] struct CanAndCon { constrained_module: ConstrainedModule, canonicalization_problems: Vec, module_docs: Option, } #[derive(Debug)] enum PlatformPath<'a> { NotSpecified, Valid(To<'a>), RootIsInterface, RootIsHosted, RootIsPlatformModule, } #[derive(Debug)] struct PlatformData { module_id: ModuleId, provides: Symbol, } #[derive(Debug, Clone, Copy)] enum MakeSpecializationsPass { Pass(u8), } impl MakeSpecializationsPass { fn inc(&mut self) { match self { &mut Self::Pass(n) => { *self = Self::Pass( n.checked_add(1) .expect("way too many specialization passes!"), ) } } } fn current_pass(&self) -> u8 { match self { MakeSpecializationsPass::Pass(n) => *n, } } } #[derive(Debug)] struct State<'a> { pub root_id: ModuleId, pub root_subs: Option, pub platform_data: Option, pub exposed_types: ExposedByModule, pub output_path: Option<&'a str>, pub platform_path: PlatformPath<'a>, pub target_info: TargetInfo, pub module_cache: ModuleCache<'a>, pub dependencies: Dependencies<'a>, pub procedures: MutMap<(Symbol, ProcLayout<'a>), Proc<'a>>, pub toplevel_expects: ToplevelExpects, pub exposed_to_host: ExposedToHost, /// This is the "final" list of IdentIds, after canonicalization and constraint gen /// have completed for a given module. pub constrained_ident_ids: IdentIdsByModule, /// From now on, these will be used by multiple threads; time to make an Arc>! pub arc_modules: Arc>>, pub arc_shorthands: Arc>>>, #[allow(unused)] pub derived_module: SharedDerivedModule, pub ident_ids_by_module: SharedIdentIdsByModule, pub declarations_by_id: MutMap, pub exposed_symbols_by_module: MutMap>, pub timings: MutMap, // Each thread gets its own layout cache. When one "pending specializations" // pass completes, it returns its layout cache so another thread can use it. // We don't bother trying to union them all together to maximize cache hits, // since the unioning process could potentially take longer than the savings. // (Granted, this has not been attempted or measured!) pub layout_caches: std::vec::Vec>, pub render: RenderTarget, pub exec_mode: ExecutionMode, /// All abilities across all modules. pub world_abilities: WorldAbilities, make_specializations_pass: MakeSpecializationsPass, // cached subs (used for builtin modules, could include packages in the future too) cached_subs: CachedSubs, } type CachedSubs = Arc)>>>; impl<'a> State<'a> { fn goal_phase(&self) -> Phase { self.exec_mode.goal_phase() } #[allow(clippy::too_many_arguments)] fn new( root_id: ModuleId, target_info: TargetInfo, exposed_types: ExposedByModule, arc_modules: Arc>>, ident_ids_by_module: SharedIdentIdsByModule, cached_subs: MutMap)>, render: RenderTarget, number_of_workers: usize, exec_mode: ExecutionMode, ) -> Self { let arc_shorthands = Arc::new(Mutex::new(MutMap::default())); let dependencies = Dependencies::new(exec_mode.goal_phase()); Self { root_id, root_subs: None, target_info, platform_data: None, output_path: None, platform_path: PlatformPath::NotSpecified, module_cache: ModuleCache::default(), dependencies, procedures: MutMap::default(), toplevel_expects: ToplevelExpects::default(), exposed_to_host: ExposedToHost::default(), exposed_types, arc_modules, arc_shorthands, derived_module: Default::default(), constrained_ident_ids: IdentIds::exposed_builtins(0), ident_ids_by_module, declarations_by_id: MutMap::default(), exposed_symbols_by_module: MutMap::default(), timings: MutMap::default(), layout_caches: std::vec::Vec::with_capacity(number_of_workers), cached_subs: Arc::new(Mutex::new(cached_subs)), render, exec_mode, make_specializations_pass: MakeSpecializationsPass::Pass(1), world_abilities: Default::default(), } } } #[derive(Debug)] pub struct ModuleTiming { pub read_roc_file: Duration, pub parse_header: Duration, pub parse_body: Duration, pub canonicalize: Duration, pub constrain: Duration, pub solve: Duration, pub find_specializations: Duration, // indexed by make specializations pass pub make_specializations: Vec, // TODO pub monomorphize: Duration, /// Total duration will always be more than the sum of the other fields, due /// to things like state lookups in between phases, waiting on other threads, etc. start_time: Instant, end_time: Instant, } impl ModuleTiming { pub fn new(start_time: Instant) -> Self { ModuleTiming { read_roc_file: Duration::default(), parse_header: Duration::default(), parse_body: Duration::default(), canonicalize: Duration::default(), constrain: Duration::default(), solve: Duration::default(), find_specializations: Duration::default(), make_specializations: Vec::with_capacity(2), start_time, end_time: start_time, // just for now; we'll overwrite this at the end } } pub fn total(&self) -> Duration { self.end_time.duration_since(self.start_time) } /// Subtract all the other fields from total_start_to_finish pub fn other(&self) -> Duration { let Self { read_roc_file, parse_header, parse_body, canonicalize, constrain, solve, find_specializations, make_specializations, start_time, end_time, } = self; let calculate = |d: Option| -> Option { make_specializations .iter() .fold(d, |d, pass_time| d?.checked_sub(*pass_time))? .checked_sub(*find_specializations)? .checked_sub(*solve)? .checked_sub(*constrain)? .checked_sub(*canonicalize)? .checked_sub(*parse_body)? .checked_sub(*parse_header)? .checked_sub(*read_roc_file) }; calculate(Some(end_time.duration_since(*start_time))).unwrap_or_default() } } /// A message sent _to_ a worker thread, describing the work to be done #[derive(Debug)] #[allow(dead_code)] enum BuildTask<'a> { LoadModule { module_name: PQModuleName<'a>, module_ids: Arc>>, shorthands: Arc>>>, ident_ids_by_module: SharedIdentIdsByModule, }, Parse { header: ModuleHeader<'a>, }, CanonicalizeAndConstrain { parsed: ParsedModule<'a>, module_ids: ModuleIds, dep_idents: IdentIdsByModule, exposed_symbols: VecSet, aliases: MutMap, abilities_store: PendingAbilitiesStore, skip_constraint_gen: bool, }, Solve { module: Module, ident_ids: IdentIds, exposed_for_module: ExposedForModule, module_timing: ModuleTiming, constraints: Constraints, constraint: ConstraintSoa, pending_derives: PendingDerives, var_store: VarStore, declarations: Declarations, dep_idents: IdentIdsByModule, cached_subs: CachedSubs, derived_module: SharedDerivedModule, }, BuildPendingSpecializations { module_timing: ModuleTiming, execution_mode: ExecutionMode, layout_cache: LayoutCache<'a>, solved_subs: Solved, imported_module_thunks: &'a [Symbol], module_id: ModuleId, ident_ids: IdentIds, decls: Declarations, exposed_to_host: ExposedToHost, exposed_by_module: ExposedByModule, abilities_store: AbilitiesStore, derived_module: SharedDerivedModule, }, MakeSpecializations { module_id: ModuleId, ident_ids: IdentIds, subs: Subs, procs_base: ProcsBase<'a>, layout_cache: LayoutCache<'a>, specializations_we_must_make: Vec>, module_timing: ModuleTiming, exposed_by_module: ExposedByModule, world_abilities: WorldAbilities, derived_module: SharedDerivedModule, }, } enum WorkerMsg { Shutdown, TaskAdded, } #[derive(Debug)] pub enum LoadingProblem<'a> { FileProblem { filename: PathBuf, error: io::ErrorKind, }, ParsingFailed(FileError<'a, SyntaxError<'a>>), UnexpectedHeader(String), MsgChannelDied, ErrJoiningWorkerThreads, TriedToImportAppModule, /// a formatted report FormattedReport(String), } pub enum Phases { /// Parse, canonicalize, check types TypeCheck, /// Parse, canonicalize, check types, monomorphize Monomorphize, } type MsgSender<'a> = Sender>; /// Add a task to the queue, and notify all the listeners. fn enqueue_task<'a>( injector: &Injector>, listeners: &[Sender], task: BuildTask<'a>, ) -> Result<(), LoadingProblem<'a>> { injector.push(task); for listener in listeners { listener .send(WorkerMsg::TaskAdded) .map_err(|_| LoadingProblem::MsgChannelDied)?; } Ok(()) } #[allow(clippy::too_many_arguments)] pub fn load_and_typecheck_str<'a>( arena: &'a Bump, filename: PathBuf, source: &'a str, src_dir: PathBuf, exposed_types: ExposedByModule, target_info: TargetInfo, render: RenderTarget, threading: Threading, ) -> Result> { use LoadResult::*; let load_start = LoadStart::from_str(arena, filename, source, src_dir)?; // this function is used specifically in the case // where we want to regenerate the cached data let cached_subs = MutMap::default(); let load_config = LoadConfig { target_info, render, threading, exec_mode: ExecutionMode::Check, }; match load(arena, load_start, exposed_types, cached_subs, load_config)? { Monomorphized(_) => unreachable!(""), TypeChecked(module) => Ok(module), } } #[derive(Clone, Copy)] pub enum PrintTarget { ColorTerminal, Generic, } pub struct LoadStart<'a> { arc_modules: Arc>>, ident_ids_by_module: SharedIdentIdsByModule, root_id: ModuleId, root_msg: Msg<'a>, src_dir: PathBuf, } impl<'a> LoadStart<'a> { pub fn from_path( arena: &'a Bump, filename: PathBuf, render: RenderTarget, ) -> Result> { let arc_modules = Arc::new(Mutex::new(PackageModuleIds::default())); let root_exposed_ident_ids = IdentIds::exposed_builtins(0); let ident_ids_by_module = Arc::new(Mutex::new(root_exposed_ident_ids)); let mut src_dir = filename.parent().unwrap().to_path_buf(); // Load the root module synchronously; we can't proceed until we have its id. let (root_id, root_msg) = { let root_start_time = Instant::now(); let res_loaded = load_filename( arena, filename, true, None, Arc::clone(&arc_modules), Arc::clone(&ident_ids_by_module), root_start_time, ); match res_loaded { Ok((module_id, msg)) => { if let Msg::Header(ModuleHeader { module_id: header_id, module_name, is_root_module, .. }) = &msg { debug_assert_eq!(*header_id, module_id); debug_assert!(is_root_module); if let ModuleNameEnum::Interface(name) = module_name { // Interface modules can have names like Foo.Bar.Baz, // in which case we need to adjust the src_dir to // remove the "Bar/Baz" directories in order to correctly // resolve this interface module's imports! let dirs_to_pop = name.as_str().matches('.').count(); for _ in 0..dirs_to_pop { src_dir.pop(); } } } (module_id, msg) } Err(LoadingProblem::ParsingFailed(problem)) => { let module_ids = Arc::try_unwrap(arc_modules) .unwrap_or_else(|_| { panic!("There were still outstanding Arc references to module_ids") }) .into_inner() .into_module_ids(); // if parsing failed, this module did not add any identifiers let root_exposed_ident_ids = IdentIds::exposed_builtins(0); let buf = to_parse_problem_report( problem, module_ids, root_exposed_ident_ids, render, ); return Err(LoadingProblem::FormattedReport(buf)); } Err(LoadingProblem::FileProblem { filename, error }) => { let buf = to_file_problem_report(&filename, error); return Err(LoadingProblem::FormattedReport(buf)); } Err(e) => return Err(e), } }; Ok(LoadStart { arc_modules, ident_ids_by_module, src_dir, root_id, root_msg, }) } pub fn from_str( arena: &'a Bump, filename: PathBuf, src: &'a str, src_dir: PathBuf, ) -> Result> { let arc_modules = Arc::new(Mutex::new(PackageModuleIds::default())); let root_exposed_ident_ids = IdentIds::exposed_builtins(0); let ident_ids_by_module = Arc::new(Mutex::new(root_exposed_ident_ids)); // Load the root module synchronously; we can't proceed until we have its id. let (root_id, root_msg) = { let root_start_time = Instant::now(); load_from_str( arena, filename, src, Arc::clone(&arc_modules), Arc::clone(&ident_ids_by_module), root_start_time, )? }; Ok(LoadStart { arc_modules, src_dir, ident_ids_by_module, root_id, root_msg, }) } } pub enum LoadResult<'a> { TypeChecked(LoadedModule), Monomorphized(MonomorphizedModule<'a>), } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Threading { Single, AllAvailable, AtMost(usize), } /// The loading process works like this, starting from the given filename (e.g. "main.roc"): /// /// 1. Open the file. /// 2. Parse the module's header. /// 3. For each of its imports, send a message on the channel to the coordinator thread, which /// will repeat this process to load that module - starting with step 1. /// 4. Add everything we were able to import unqualified to the module's default scope. /// 5. Parse the module's defs. /// 6. Canonicalize the module. /// 7. Before type checking, block on waiting for type checking to complete on all imports. /// (Since Roc doesn't allow cyclic dependencies, this cannot deadlock.) /// 8. Type check the module and create type annotations for its top-level declarations. /// 9. Report the completed type annotation to the coordinator thread, so other modules /// that are waiting in step 7 can unblock. /// /// The loaded_modules argument specifies which modules have already been loaded. /// It typically contains *at least* the standard modules, but is empty when loading /// the standard modules themselves. /// /// If we're just type-checking everything (e.g. running `roc check` at the command line), /// we can stop there. However, if we're generating code, then there are additional steps. /// /// 10. After reporting the completed type annotation, we have all the information necessary /// to monomorphize. However, since we want to monomorphize in parallel without /// duplicating work, we do monomorphization in two steps. First, we go through and /// determine all the specializations this module *wants*. We compute the hashes /// and report them to the coordinator thread, along with the mono::expr::Expr values of /// the current function's body. At this point, we have not yet begun to assemble Procs; /// all we've done is send a list of requetsted specializations to the coordinator. /// 11. The coordinator works through the specialization requests in parallel, adding them /// to a global map once they're finished. Performing one specialization may result /// in requests for others; these are added to the queue and worked through as normal. /// This process continues until *both* all modules have reported that they've finished /// adding specialization requests to the queue, *and* the queue is empty (including /// of any requests that were added in the course of completing other requests). Now /// we have a map of specializations, and everything was assembled in parallel with /// no unique specialization ever getting assembled twice (meaning no wasted effort). /// /// a. Note that this might mean that we have to specialize certain modules multiple times. /// When might this happen? Well, abilities can introduce implicit edges in the dependency /// graph, and even cycles. For example, suppose module Ab provides "ability1" and a function /// "f" that uses "ability1", and module App implements "ability1" and calls "f" with the /// implementing type. Then the specialization of "Ab#f" depends on the specialization of /// "ability1" back in the App module. /// 12. Now that we have our final map of specializations, we can proceed to code gen! /// As long as the specializations are stored in a per-ModuleId map, we can also /// parallelize this code gen. (e.g. in dev builds, building separate LLVM modules /// and then linking them together, and possibly caching them by the hash of their /// specializations, so if none of their specializations changed, we don't even need /// to rebuild the module and can link in the cached one directly.) #[allow(clippy::too_many_arguments)] pub fn load<'a>( arena: &'a Bump, load_start: LoadStart<'a>, exposed_types: ExposedByModule, cached_subs: MutMap)>, load_config: LoadConfig, ) -> Result, LoadingProblem<'a>> { enum Threads { Single, Many(usize), } let threads = { if cfg!(target_family = "wasm") { // When compiling to wasm, we cannot spawn extra threads // so we have a single-threaded implementation Threads::Single } else { match std::thread::available_parallelism().map(|v| v.get()) { Err(_) => Threads::Single, Ok(0) => unreachable!("NonZeroUsize"), Ok(1) => Threads::Single, Ok(reported) => match load_config.threading { Threading::Single => Threads::Single, Threading::AllAvailable => Threads::Many(reported), Threading::AtMost(at_most) => Threads::Many(Ord::min(reported, at_most)), }, } } }; match threads { Threads::Single => load_single_threaded( arena, load_start, exposed_types, load_config.target_info, cached_subs, load_config.render, load_config.exec_mode, ), Threads::Many(threads) => load_multi_threaded( arena, load_start, exposed_types, load_config.target_info, cached_subs, load_config.render, threads, load_config.exec_mode, ), } } /// Load using only a single thread; used when compiling to webassembly #[allow(clippy::too_many_arguments)] pub fn load_single_threaded<'a>( arena: &'a Bump, load_start: LoadStart<'a>, exposed_types: ExposedByModule, target_info: TargetInfo, cached_subs: MutMap)>, render: RenderTarget, exec_mode: ExecutionMode, ) -> Result, LoadingProblem<'a>> { let LoadStart { arc_modules, ident_ids_by_module, root_id, root_msg, src_dir, .. } = load_start; let (msg_tx, msg_rx) = bounded(1024); msg_tx .send(root_msg) .map_err(|_| LoadingProblem::MsgChannelDied)?; let number_of_workers = 1; let mut state = State::new( root_id, target_info, exposed_types, arc_modules, ident_ids_by_module, cached_subs, render, number_of_workers, exec_mode, ); // We'll add tasks to this, and then worker threads will take tasks from it. let injector = Injector::new(); let (worker_msg_tx, worker_msg_rx) = bounded(1024); let worker_listener = worker_msg_tx; let worker_listeners = arena.alloc([worker_listener]); let worker = Worker::new_fifo(); let stealer = worker.stealer(); let stealers = &[stealer]; // now we just manually interleave stepping the state "thread" and the worker "thread" loop { match state_thread_step(arena, state, worker_listeners, &injector, &msg_tx, &msg_rx) { Ok(ControlFlow::Break(done)) => return Ok(done), Ok(ControlFlow::Continue(new_state)) => { state = new_state; } Err(e) => return Err(e), } // then check if the worker can step let control_flow = worker_task_step( arena, &worker, &injector, stealers, &worker_msg_rx, &msg_tx, &src_dir, target_info, ); match control_flow { Ok(ControlFlow::Break(())) => panic!("the worker should not break!"), Ok(ControlFlow::Continue(())) => { // progress was made } Err(e) => return Err(e), } } } fn state_thread_step<'a>( arena: &'a Bump, state: State<'a>, worker_listeners: &'a [Sender], injector: &Injector>, msg_tx: &crossbeam::channel::Sender>, msg_rx: &crossbeam::channel::Receiver>, ) -> Result, State<'a>>, LoadingProblem<'a>> { match msg_rx.try_recv() { Ok(msg) => { match msg { Msg::FinishedAllTypeChecking { solved_subs, exposed_vars_by_symbol, exposed_aliases_by_symbol, exposed_types_storage, resolved_implementations, dep_idents, documentation, abilities_store, } => { // We're done! There should be no more messages pending. debug_assert!(msg_rx.is_empty()); let exposed_aliases_by_symbol = exposed_aliases_by_symbol .into_iter() .map(|(k, (_, v))| (k, v)) .collect(); let typechecked = finish( state, solved_subs, exposed_aliases_by_symbol, exposed_vars_by_symbol, exposed_types_storage, resolved_implementations, dep_idents, documentation, abilities_store, ); Ok(ControlFlow::Break(LoadResult::TypeChecked(typechecked))) } Msg::FinishedAllSpecialization { subs, exposed_to_host, } => { // We're done! There should be no more messages pending. debug_assert!(msg_rx.is_empty()); let monomorphized = finish_specialization(state, subs, exposed_to_host)?; Ok(ControlFlow::Break(LoadResult::Monomorphized(monomorphized))) } Msg::FailedToReadFile { filename, error } => { let buf = to_file_problem_report(&filename, error); Err(LoadingProblem::FormattedReport(buf)) } Msg::FailedToParse(problem) => { let module_ids = (*state.arc_modules).lock().clone().into_module_ids(); let buf = to_parse_problem_report( problem, module_ids, state.constrained_ident_ids, state.render, ); Err(LoadingProblem::FormattedReport(buf)) } msg => { // This is where most of the main thread's work gets done. // Everything up to this point has been setting up the threading // system which lets this logic work efficiently. let arc_modules = state.arc_modules.clone(); let render = state.render; let res_state = update( state, msg, msg_tx.clone(), injector, worker_listeners, arena, ); match res_state { Ok(new_state) => Ok(ControlFlow::Continue(new_state)), Err(LoadingProblem::ParsingFailed(problem)) => { let module_ids = Arc::try_unwrap(arc_modules) .unwrap_or_else(|_| { panic!( r"There were still outstanding Arc references to module_ids" ) }) .into_inner() .into_module_ids(); // if parsing failed, this module did not add anything to IdentIds let root_exposed_ident_ids = IdentIds::exposed_builtins(0); let buf = to_parse_problem_report( problem, module_ids, root_exposed_ident_ids, render, ); Err(LoadingProblem::FormattedReport(buf)) } Err(e) => Err(e), } } } } Err(err) => match err { crossbeam::channel::TryRecvError::Empty => Ok(ControlFlow::Continue(state)), crossbeam::channel::TryRecvError::Disconnected => Err(LoadingProblem::MsgChannelDied), }, } } #[allow(clippy::too_many_arguments)] fn load_multi_threaded<'a>( arena: &'a Bump, load_start: LoadStart<'a>, exposed_types: ExposedByModule, target_info: TargetInfo, cached_subs: MutMap)>, render: RenderTarget, available_threads: usize, exec_mode: ExecutionMode, ) -> Result, LoadingProblem<'a>> { let LoadStart { arc_modules, ident_ids_by_module, root_id, root_msg, src_dir, .. } = load_start; let (msg_tx, msg_rx) = bounded(1024); msg_tx .send(root_msg) .map_err(|_| LoadingProblem::MsgChannelDied)?; // Reserve one CPU for the main thread, and let all the others be eligible // to spawn workers. let available_workers = available_threads - 1; let num_workers = match env::var("ROC_NUM_WORKERS") { Ok(env_str) => env_str .parse::() .unwrap_or(available_workers) .min(available_workers), Err(_) => available_workers, }; assert!( num_workers >= 1, "`load_multi_threaded` needs at least one worker" ); let mut state = State::new( root_id, target_info, exposed_types, arc_modules, ident_ids_by_module, cached_subs, render, num_workers, exec_mode, ); // an arena for every worker, stored in an arena-allocated bumpalo vec to make the lifetimes work let arenas = std::iter::repeat_with(Bump::new).take(num_workers); let worker_arenas = arena.alloc(bumpalo::collections::Vec::from_iter_in(arenas, arena)); // We'll add tasks to this, and then worker threads will take tasks from it. let injector = Injector::new(); // We need to allocate worker *queues* on the main thread and then move them // into the worker threads, because those workers' stealers need to be // shared between all threads, and this coordination work is much easier // on the main thread. let mut worker_queues = bumpalo::collections::Vec::with_capacity_in(num_workers, arena); let mut stealers = bumpalo::collections::Vec::with_capacity_in(num_workers, arena); for _ in 0..num_workers { let worker = Worker::new_fifo(); stealers.push(worker.stealer()); worker_queues.push(worker); } // Get a reference to the completed stealers, so we can send that // reference to each worker. (Slices are Sync, but bumpalo Vecs are not.) let stealers = stealers.into_bump_slice(); let it = worker_arenas.iter_mut(); { thread::scope(|thread_scope| { let mut worker_listeners = bumpalo::collections::Vec::with_capacity_in(num_workers, arena); for worker_arena in it { let msg_tx = msg_tx.clone(); let worker = worker_queues.pop().unwrap(); let (worker_msg_tx, worker_msg_rx) = bounded(1024); worker_listeners.push(worker_msg_tx); // We only want to move a *reference* to the main task queue's // injector in the thread, not the injector itself // (since other threads need to reference it too). Same with src_dir. let injector = &injector; let src_dir = &src_dir; // Record this thread's handle so the main thread can join it later. let res_join_handle = thread_scope .builder() .stack_size(EXPANDED_STACK_SIZE) .spawn(move |_| { // will process messages until we run out worker_task( worker_arena, worker, injector, stealers, worker_msg_rx, msg_tx, src_dir, target_info, ) }); res_join_handle.unwrap(); } // We've now distributed one worker queue to each thread. // There should be no queues left to distribute! debug_assert!(worker_queues.is_empty()); drop(worker_queues); // Grab a reference to these Senders outside the loop, so we can share // it across each iteration of the loop. let worker_listeners = worker_listeners.into_bump_slice(); let msg_tx = msg_tx.clone(); macro_rules! shut_down_worker_threads { () => { for listener in worker_listeners { listener .send(WorkerMsg::Shutdown) .map_err(|_| LoadingProblem::MsgChannelDied)?; } }; } // The root module will have already queued up messages to process, // and processing those messages will in turn queue up more messages. loop { match state_thread_step(arena, state, worker_listeners, &injector, &msg_tx, &msg_rx) { Ok(ControlFlow::Break(load_result)) => { shut_down_worker_threads!(); return Ok(load_result); } Ok(ControlFlow::Continue(new_state)) => { state = new_state; continue; } Err(e) => { shut_down_worker_threads!(); return Err(e); } } } }) } .unwrap() } #[allow(clippy::too_many_arguments)] fn worker_task_step<'a>( worker_arena: &'a Bump, worker: &Worker>, injector: &Injector>, stealers: &[Stealer>], worker_msg_rx: &crossbeam::channel::Receiver, msg_tx: &MsgSender<'a>, src_dir: &Path, target_info: TargetInfo, ) -> Result, LoadingProblem<'a>> { match worker_msg_rx.try_recv() { Ok(msg) => { match msg { WorkerMsg::Shutdown => { // We've finished all our work. It's time to // shut down the thread, so when the main thread // blocks on joining with all the worker threads, // it can finally exit too! Ok(ControlFlow::Break(())) } WorkerMsg::TaskAdded => { // Find a task - either from this thread's queue, // or from the main queue, or from another worker's // queue - and run it. // // There might be no tasks to work on! That could // happen if another thread is working on a task // which will later result in more tasks being // added. In that case, do nothing, and keep waiting // until we receive a Shutdown message. if let Some(task) = find_task(worker, injector, stealers) { let result = run_task(task, worker_arena, src_dir, msg_tx.clone(), target_info); match result { Ok(()) => {} Err(LoadingProblem::MsgChannelDied) => { panic!("Msg channel closed unexpectedly.") } Err(LoadingProblem::ParsingFailed(problem)) => { msg_tx.send(Msg::FailedToParse(problem)).unwrap(); } Err(LoadingProblem::FileProblem { filename, error }) => { msg_tx .send(Msg::FailedToReadFile { filename, error }) .unwrap(); } Err(other) => { return Err(other); } } } Ok(ControlFlow::Continue(())) } } } Err(err) => match err { crossbeam::channel::TryRecvError::Empty => Ok(ControlFlow::Continue(())), crossbeam::channel::TryRecvError::Disconnected => Ok(ControlFlow::Break(())), }, } } #[allow(clippy::too_many_arguments)] fn worker_task<'a>( worker_arena: &'a Bump, worker: Worker>, injector: &Injector>, stealers: &[Stealer>], worker_msg_rx: crossbeam::channel::Receiver, msg_tx: MsgSender<'a>, src_dir: &Path, target_info: TargetInfo, ) -> Result<(), LoadingProblem<'a>> { // Keep listening until we receive a Shutdown msg for msg in worker_msg_rx.iter() { match msg { WorkerMsg::Shutdown => { // We've finished all our work. It's time to // shut down the thread, so when the main thread // blocks on joining with all the worker threads, // it can finally exit too! return Ok(()); } WorkerMsg::TaskAdded => { // Find a task - either from this thread's queue, // or from the main queue, or from another worker's // queue - and run it. // // There might be no tasks to work on! That could // happen if another thread is working on a task // which will later result in more tasks being // added. In that case, do nothing, and keep waiting // until we receive a Shutdown message. if let Some(task) = find_task(&worker, injector, stealers) { let result = run_task(task, worker_arena, src_dir, msg_tx.clone(), target_info); match result { Ok(()) => {} Err(LoadingProblem::MsgChannelDied) => { panic!("Msg channel closed unexpectedly.") } Err(LoadingProblem::ParsingFailed(problem)) => { msg_tx.send(Msg::FailedToParse(problem)).unwrap(); } Err(LoadingProblem::FileProblem { filename, error }) => { msg_tx .send(Msg::FailedToReadFile { filename, error }) .unwrap(); } Err(other) => { return Err(other); } } } } } } Ok(()) } fn start_tasks<'a>( arena: &'a Bump, state: &mut State<'a>, work: MutSet<(ModuleId, Phase)>, injector: &Injector>, worker_listeners: &'a [Sender], ) -> Result<(), LoadingProblem<'a>> { for (module_id, phase) in work { for task in start_phase(module_id, phase, arena, state) { enqueue_task(injector, worker_listeners, task)? } } Ok(()) } macro_rules! debug_print_ir { ($state:expr, $flag:path) => { dbg_do!($flag, { let procs_string = $state .procedures .values() .map(|proc| proc.to_pretty(200)) .collect::>(); let result = procs_string.join("\n"); eprintln!("{}", result); }) }; } /// Report modules that are imported, but from which nothing is used fn report_unused_imported_modules<'a>( state: &mut State<'a>, module_id: ModuleId, constrained_module: &ConstrainedModule, ) { let mut unused_imported_modules = constrained_module.imported_modules.clone(); for symbol in constrained_module.module.referenced_values.iter() { unused_imported_modules.remove(&symbol.module_id()); } for symbol in constrained_module.module.referenced_types.iter() { unused_imported_modules.remove(&symbol.module_id()); } let existing = match state.module_cache.can_problems.entry(module_id) { Vacant(entry) => entry.insert(std::vec::Vec::new()), Occupied(entry) => entry.into_mut(), }; for (unused, region) in unused_imported_modules.drain() { if !unused.is_builtin() { existing.push(roc_problem::can::Problem::UnusedImport(unused, region)); } } } fn update<'a>( mut state: State<'a>, msg: Msg<'a>, msg_tx: MsgSender<'a>, injector: &Injector>, worker_listeners: &'a [Sender], arena: &'a Bump, ) -> Result, LoadingProblem<'a>> { use self::Msg::*; match msg { Many(messages) => { // enqueue all these message for msg in messages { msg_tx .send(msg) .map_err(|_| LoadingProblem::MsgChannelDied)?; } Ok(state) } Header(header) => { use HeaderFor::*; log!("loaded header for {:?}", header.module_id); let home = header.module_id; let mut work = MutSet::default(); { let mut shorthands = (*state.arc_shorthands).lock(); for (shorthand, package_name) in header.packages.iter() { shorthands.insert(shorthand, *package_name); } if let Platform { config_shorthand, .. } = header.header_for { work.extend(state.dependencies.notify_package(config_shorthand)); } } match header.header_for { App { to_platform } => { debug_assert!(matches!(state.platform_path, PlatformPath::NotSpecified)); state.platform_path = PlatformPath::Valid(to_platform); } Platform { main_for_host, .. } => { debug_assert!(matches!(state.platform_data, None)); state.platform_data = Some(PlatformData { module_id: header.module_id, provides: main_for_host, }); if header.is_root_module { debug_assert!(matches!(state.platform_path, PlatformPath::NotSpecified)); state.platform_path = PlatformPath::RootIsPlatformModule; } } Builtin { .. } | Interface => { if header.is_root_module { debug_assert!(matches!(state.platform_path, PlatformPath::NotSpecified)); state.platform_path = PlatformPath::RootIsInterface; } } Hosted { .. } => { if header.is_root_module { debug_assert!(matches!(state.platform_path, PlatformPath::NotSpecified)); state.platform_path = PlatformPath::RootIsHosted; } } } // store an ID to name mapping, so we know the file to read when fetching dependencies' headers for (name, id) in header.deps_by_name.iter() { state.module_cache.module_names.insert(*id, name.clone()); } // This was a dependency. Write it down and keep processing messages. let mut exposed_symbols: VecSet = VecSet::with_capacity(header.exposes.len()); // TODO can we avoid this loop by storing them as a Set in Header to begin with? for symbol in header.exposes.iter() { exposed_symbols.insert(*symbol); } // NOTE we currently re-parse the headers when a module is imported twice. // We need a proper solution that marks a phase as in-progress so it's not repeated // debug_assert!(!state.exposed_symbols_by_module.contains_key(&home)); state .exposed_symbols_by_module .insert(home, exposed_symbols); // add the prelude let mut header = header; if ![ModuleId::RESULT, ModuleId::BOOL].contains(&header.module_id) { header .package_qualified_imported_modules .insert(PackageQualified::Unqualified(ModuleId::RESULT)); header .imported_modules .insert(ModuleId::RESULT, Region::zero()); header.exposed_imports.insert( Ident::from("Result"), (Symbol::RESULT_RESULT, Region::zero()), ); } if ![ModuleId::NUM, ModuleId::BOOL, ModuleId::RESULT].contains(&header.module_id) { header .package_qualified_imported_modules .insert(PackageQualified::Unqualified(ModuleId::NUM)); header .imported_modules .insert(ModuleId::NUM, Region::zero()); for (type_name, symbol) in PRELUDE_TYPES { header .exposed_imports .insert(Ident::from(type_name), (symbol, Region::zero())); } } if header.module_id != ModuleId::BOOL { header .package_qualified_imported_modules .insert(PackageQualified::Unqualified(ModuleId::BOOL)); header .imported_modules .insert(ModuleId::BOOL, Region::zero()); header .exposed_imports .insert(Ident::from("Bool"), (Symbol::BOOL_BOOL, Region::zero())); } if header.module_id == ModuleId::NUM { header .exposed_imports .insert(Ident::from("List"), (Symbol::LIST_LIST, Region::zero())); } if !header.module_id.is_builtin() { header .package_qualified_imported_modules .insert(PackageQualified::Unqualified(ModuleId::BOX)); header .imported_modules .insert(ModuleId::BOX, Region::zero()); header .package_qualified_imported_modules .insert(PackageQualified::Unqualified(ModuleId::STR)); header .imported_modules .insert(ModuleId::STR, Region::zero()); header .package_qualified_imported_modules .insert(PackageQualified::Unqualified(ModuleId::DICT)); header .imported_modules .insert(ModuleId::DICT, Region::zero()); header .exposed_imports .insert(Ident::from("Dict"), (Symbol::DICT_DICT, Region::zero())); header .package_qualified_imported_modules .insert(PackageQualified::Unqualified(ModuleId::SET)); header .imported_modules .insert(ModuleId::SET, Region::zero()); header .exposed_imports .insert(Ident::from("Set"), (Symbol::SET_SET, Region::zero())); header .package_qualified_imported_modules .insert(PackageQualified::Unqualified(ModuleId::LIST)); header .imported_modules .insert(ModuleId::LIST, Region::zero()); header .package_qualified_imported_modules .insert(PackageQualified::Unqualified(ModuleId::ENCODE)); header .imported_modules .insert(ModuleId::ENCODE, Region::zero()); } state .module_cache .imports .entry(header.module_id) .or_default() .extend( header .package_qualified_imported_modules .iter() .map(|x| *x.as_inner()), ); work.extend(state.dependencies.add_module( header.module_id, &header.package_qualified_imported_modules, state.exec_mode.goal_phase(), )); state.module_cache.headers.insert(header.module_id, header); start_tasks(arena, &mut state, work, injector, worker_listeners)?; let work = state.dependencies.notify(home, Phase::LoadHeader); start_tasks(arena, &mut state, work, injector, worker_listeners)?; Ok(state) } Parsed(parsed) => { state .module_cache .sources .insert(parsed.module_id, (parsed.module_path.clone(), parsed.src)); // If this was an app module, set the output path to be // the module's declared "name". // // e.g. for `app "blah"` we should generate an output file named "blah" match &parsed.module_name { ModuleNameEnum::App(output_str) => match output_str { StrLiteral::PlainLine(path) => { state.output_path = Some(path); } _ => { todo!("TODO gracefully handle a malformed string literal after `app` keyword."); } }, ModuleNameEnum::Platform | ModuleNameEnum::Interface(_) | ModuleNameEnum::Hosted(_) => {} } let module_id = parsed.module_id; state.module_cache.parsed.insert(parsed.module_id, parsed); let work = state.dependencies.notify(module_id, Phase::Parse); start_tasks(arena, &mut state, work, injector, worker_listeners)?; Ok(state) } CanonicalizedAndConstrained(CanAndCon { constrained_module, canonicalization_problems, module_docs, }) => { let module_id = constrained_module.module.module_id; log!("generated constraints for {:?}", module_id); state .module_cache .can_problems .insert(module_id, canonicalization_problems); if let Some(docs) = module_docs { state.module_cache.documentation.insert(module_id, docs); } report_unused_imported_modules(&mut state, module_id, &constrained_module); state .module_cache .aliases .insert(module_id, constrained_module.module.aliases.clone()); state .module_cache .pending_abilities .insert(module_id, constrained_module.module.abilities_store.clone()); state .module_cache .constrained .insert(module_id, constrained_module); let work = state .dependencies .notify(module_id, Phase::CanonicalizeAndConstrain); start_tasks(arena, &mut state, work, injector, worker_listeners)?; Ok(state) } SolvedTypes { module_id, ident_ids, solved_module, solved_subs, decls, dep_idents, mut module_timing, abilities_store, loc_expects, } => { log!("solved types for {:?}", module_id); module_timing.end_time = Instant::now(); state .module_cache .type_problems .insert(module_id, solved_module.problems); if !loc_expects.is_empty() { let (path, _) = state.module_cache.sources.get(&module_id).unwrap(); let expectations = Expectations { expectations: loc_expects, subs: solved_subs.clone().into_inner(), path: path.to_owned(), ident_ids: ident_ids.clone(), }; state .module_cache .expectations .insert(module_id, expectations); } let work = state.dependencies.notify(module_id, Phase::SolveTypes); // if there is a platform, the `platform` module provides host-exposed, // otherwise the App module exposes host-exposed let is_host_exposed = match state.platform_data { None => module_id == state.root_id, Some(ref platform_data) => module_id == platform_data.module_id, }; if is_host_exposed { state.exposed_to_host.values.extend( solved_module .exposed_vars_by_symbol .iter() .filter_map(|(k, v)| { if abilities_store.is_specialization_name(*k) { None } else { Some((*k, *v)) } }), ); state .exposed_to_host .closure_types .extend(solved_module.aliases.keys().copied()); } 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()); state.timings.insert(module_id, module_timing); if matches!(state.exec_mode, ExecutionMode::ExecutableIfCheck) { // We there may outstanding modules in the typecheked cache whose ident IDs // aren't registered; transfer all of their idents over to the state, since // we're now done and ready to report errors. for ( module_id, TypeCheckedModule { ident_ids, module_timing, .. }, ) in state.module_cache.typechecked.drain() { state.constrained_ident_ids.insert(module_id, ident_ids); state.timings.insert(module_id, module_timing); } } let documentation = { let mut empty = MutMap::default(); std::mem::swap(&mut empty, &mut state.module_cache.documentation); empty }; msg_tx .send(Msg::FinishedAllTypeChecking { solved_subs, exposed_vars_by_symbol: solved_module.exposed_vars_by_symbol, exposed_aliases_by_symbol: solved_module.aliases, exposed_types_storage: solved_module.exposed_types, resolved_implementations: solved_module.solved_implementations, dep_idents, documentation, abilities_store, }) .map_err(|_| LoadingProblem::MsgChannelDied)?; // bookkeeping state.declarations_by_id.insert(module_id, decls); state.constrained_ident_ids.insert(module_id, ident_ids); // As far as type-checking goes, once we've solved // the originally requested module, we're all done! return Ok(state); } else { state.exposed_types.insert( module_id, ExposedModuleTypes { exposed_types_storage_subs: solved_module.exposed_types, resolved_implementations: solved_module.solved_implementations, }, ); if state.goal_phase() > Phase::SolveTypes || matches!(state.exec_mode, ExecutionMode::ExecutableIfCheck) { let layout_cache = state .layout_caches .pop() .unwrap_or_else(|| LayoutCache::new(state.target_info)); let typechecked = TypeCheckedModule { module_id, layout_cache, module_timing, solved_subs, decls, ident_ids, abilities_store, }; state .module_cache .typechecked .insert(module_id, typechecked); } else { state.constrained_ident_ids.insert(module_id, ident_ids); 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)?; } Ok(state) } FoundSpecializations { module_id, procs_base, solved_subs, ident_ids, layout_cache, module_timing, abilities_store, toplevel_expects, } => { log!("found specializations for {:?}", module_id); let subs = solved_subs.into_inner(); state.toplevel_expects.pure.extend(toplevel_expects.pure); state.toplevel_expects.fx.extend(toplevel_expects.fx); state .module_cache .top_level_thunks .entry(module_id) .or_default() .extend(procs_base.module_thunks.iter().copied()); let found_specializations_module = FoundSpecializationsModule { ident_ids, layout_cache, procs_base, subs, module_timing, abilities_store, }; state .module_cache .found_specializations .insert(module_id, found_specializations_module); let work = state .dependencies .notify(module_id, Phase::FindSpecializations); start_tasks(arena, &mut state, work, injector, worker_listeners)?; Ok(state) } MadeSpecializations { module_id, ident_ids, mut update_mode_ids, subs, procs_base, procedures, external_specializations_requested, module_timing, layout_cache, .. } => { debug_assert!(state.goal_phase() == Phase::MakeSpecializations); log!("made specializations for {:?}", module_id); // in the future, layouts will be in SoA form and we'll want to hold on to this data let _ = layout_cache; state.procedures.extend(procedures); state.module_cache.late_specializations.insert( module_id, LateSpecializationsModule { ident_ids, module_timing, subs, layout_cache, procs_base, }, ); let work = state .dependencies .notify(module_id, Phase::MakeSpecializations); for (module_id, requested) in external_specializations_requested { let existing = match state .module_cache .external_specializations_requested .entry(module_id) { Vacant(entry) => entry.insert(vec![]), Occupied(entry) => entry.into_mut(), }; existing.push(requested); } enum NextStep { Done, RelaunchPhase, MakingInPhase, } let all_work_done = work.is_empty() && state.dependencies.solved_all(); let next_step = if all_work_done { if state .module_cache .external_specializations_requested .is_empty() { NextStep::Done } else { NextStep::RelaunchPhase } } else { NextStep::MakingInPhase }; match next_step { NextStep::Done => { // We are all done with specializations across all modules. // Insert post-specialization operations and report our completion. if !state .module_cache .external_specializations_requested .is_empty() { internal_error!( "No more work left, but external specializations left over: {:?}", state.module_cache.external_specializations_requested ); } // Flush late-specialization module information to the top-level of the state // where it will be visible to others, since we don't need late specialization // anymore. for ( module_id, LateSpecializationsModule { ident_ids, subs, module_timing, layout_cache: _, procs_base: _, }, ) in state.module_cache.late_specializations.drain() { state.constrained_ident_ids.insert(module_id, ident_ids); if module_id == state.root_id { state.root_subs = Some(subs); } state.timings.insert(module_id, module_timing); } log!("specializations complete from {:?}", module_id); debug_print_ir!(state, ROC_PRINT_IR_AFTER_SPECIALIZATION); let ident_ids = state.constrained_ident_ids.get_mut(&module_id).unwrap(); Proc::insert_reset_reuse_operations( arena, module_id, ident_ids, &mut update_mode_ids, &mut state.procedures, ); debug_print_ir!(state, ROC_PRINT_IR_AFTER_RESET_REUSE); Proc::insert_refcount_operations( arena, module_id, ident_ids, &mut update_mode_ids, &mut state.procedures, ); debug_print_ir!(state, ROC_PRINT_IR_AFTER_REFCOUNT); // This is not safe with the new non-recursive RC updates that we do for tag unions // // Proc::optimize_refcount_operations( // arena, // module_id, // &mut ident_ids, // &mut state.procedures, // ); // use the subs of the root module; // this is used in the repl to find the type of `main` let subs = state.root_subs.clone().unwrap(); msg_tx .send(Msg::FinishedAllSpecialization { subs, // TODO thread through mono problems exposed_to_host: state.exposed_to_host.clone(), }) .map_err(|_| LoadingProblem::MsgChannelDied)?; Ok(state) } NextStep::RelaunchPhase => { // We passed through the dependency graph of modules to be specialized, but // there are still specializations left over. Restart the make specializations // phase in reverse topological order. // // This happens due to abilities. In detail, consider // // # Default module // interface Default exposes [default, getDefault] // // Default has default : {} -> a | a has Default // // getDefault = \{} -> default {} // // # App module // app "test" provides [main] imports [Default.{default, getDefault}] // // Foo := {} // // default = \{} -> @Foo {} // // main = // f : Foo // f = getDefault {} // f // // The syntactic make specializations graph (based on imports) will be // App -> Default, and in a pass will build the specializations `App#main` and // `Default#getDefault for Foo`. But now notice that `Default#getDefault` will // have gained an implicit dependency on the specialized `default` for `Foo`, // `App#Foo#default`. So for abilities, the syntactic import graph is not // enough to express the entire dependency graph. // // The simplest way to resolve these leftover, possibly circular // specializations is to relaunch the make-specializations phase in the import // order until there are no more specializations left to be made. This is a bit // unfortunate in that we may look again into modules that don't need any // specializations made, but there are also some nice properties: // // - no more specializations will be made than needed // - the number of phase relaunches scales linearly with the largest number of // "bouncing back and forth" between ability calls, which is likely to be // small in practice // - the phases will always terminate. suppose they didn't; then there must be // an infinite chain of calls all of which have different layouts. In Roc // this can only be true if the calls are all mutually recursive, and // furthermore are polymorphically recursive. But polymorphic recursion is // illegal in Roc, will have been enforced during type inference. if state .module_cache .external_specializations_requested .is_empty() { internal_error!( "No specializations left over, but we were told to loop making specializations" ); } log!("re-launching specializations pass"); state.make_specializations_pass.inc(); let work = state.dependencies.reload_make_specialization_pass(); start_tasks(arena, &mut state, work, injector, worker_listeners)?; Ok(state) } NextStep::MakingInPhase => { start_tasks(arena, &mut state, work, injector, worker_listeners)?; Ok(state) } } } Msg::FinishedAllTypeChecking { .. } => { unreachable!(); } Msg::FinishedAllSpecialization { .. } => { unreachable!(); } Msg::FailedToParse(_) => { unreachable!(); } Msg::FailedToReadFile { .. } => { unreachable!(); } } } fn finish_specialization( state: State, subs: Subs, exposed_to_host: ExposedToHost, ) -> Result { if false { println!( "total Type clones: {} ", roc_types::types::get_type_clone_count() ); } let module_ids = Arc::try_unwrap(state.arc_modules) .unwrap_or_else(|_| panic!("There were still outstanding Arc references to module_ids")) .into_inner() .into_module_ids(); let mut all_ident_ids = state.constrained_ident_ids; // Associate the ident IDs from the derived synth module let (_, derived_synth_ident_ids) = Arc::try_unwrap(state.derived_module) .unwrap_or_else(|_| internal_error!("Outstanding references to the derived module")) .into_inner() .unwrap() .decompose(); ModuleId::DERIVED_SYNTH.register_debug_idents(&derived_synth_ident_ids); all_ident_ids.insert(ModuleId::DERIVED_SYNTH, derived_synth_ident_ids); let interns = Interns { module_ids, all_ident_ids, }; let State { toplevel_expects, procedures, module_cache, output_path, platform_path, platform_data, exec_mode, .. } = state; let ModuleCache { expectations, type_problems, can_problems, sources, .. } = module_cache; let sources: MutMap)> = sources .into_iter() .map(|(id, (path, src))| (id, (path, src.into()))) .collect(); let entry_point = { match exec_mode { ExecutionMode::Test => EntryPoint::Test, ExecutionMode::Executable | ExecutionMode::ExecutableIfCheck => { let path_to_platform = { use PlatformPath::*; let package_name = match platform_path { Valid(To::ExistingPackage(shorthand)) => { match (*state.arc_shorthands).lock().get(shorthand) { Some(p_or_p) => *p_or_p, None => unreachable!(), } } Valid(To::NewPackage(p_or_p)) => p_or_p, other => { let buf = to_missing_platform_report(state.root_id, other); return Err(LoadingProblem::FormattedReport(buf)); } }; package_name.into() }; let platform_path = Path::new(path_to_platform).into(); let symbol = match platform_data { None => { debug_assert_eq!(exposed_to_host.values.len(), 1); *exposed_to_host.values.iter().next().unwrap().0 } Some(PlatformData { provides, .. }) => provides, }; match procedures.keys().find(|(s, _)| *s == symbol) { Some((_, layout)) => EntryPoint::Executable { layout: *layout, symbol, platform_path, }, None => { // the entry point is not specialized. This can happen if the repl output // is a function value EntryPoint::Executable { layout: roc_mono::ir::ProcLayout { arguments: &[], result: Layout::struct_no_name_order(&[]), captures_niche: CapturesNiche::no_niche(), }, symbol, platform_path, } } } } ExecutionMode::Check => unreachable!(), } }; let output_path = match output_path { Some(path_str) => Path::new(path_str).into(), None => current_dir().unwrap().join(DEFAULT_APP_OUTPUT_PATH).into(), }; Ok(MonomorphizedModule { can_problems, type_problems, output_path, expectations, exposed_to_host, module_id: state.root_id, subs, interns, procedures, entry_point, sources, timings: state.timings, toplevel_expects, }) } #[allow(clippy::too_many_arguments)] fn finish( mut state: State, solved: Solved, exposed_aliases_by_symbol: MutMap, exposed_vars_by_symbol: Vec<(Symbol, Variable)>, exposed_types_storage: ExposedTypesStorageSubs, resolved_implementations: ResolvedImplementations, dep_idents: IdentIdsByModule, documentation: MutMap, abilities_store: AbilitiesStore, ) -> LoadedModule { let module_ids = Arc::try_unwrap(state.arc_modules) .unwrap_or_else(|_| panic!("There were still outstanding Arc references to module_ids")) .into_inner() .into_module_ids(); // Associate the ident IDs from the derived synth module let (_, derived_synth_ident_ids) = Arc::try_unwrap(state.derived_module) .unwrap_or_else(|_| internal_error!("Outstanding references to the derived module")) .into_inner() .unwrap() .decompose(); ModuleId::DERIVED_SYNTH.register_debug_idents(&derived_synth_ident_ids); state .constrained_ident_ids .insert(ModuleId::DERIVED_SYNTH, derived_synth_ident_ids); let interns = Interns { module_ids, all_ident_ids: state.constrained_ident_ids, }; let sources = state .module_cache .sources .into_iter() .map(|(id, (path, src))| (id, (path, src.into()))) .collect(); let exposed_values = exposed_vars_by_symbol.iter().map(|x| x.0).collect(); LoadedModule { module_id: state.root_id, interns, solved, can_problems: state.module_cache.can_problems, type_problems: state.module_cache.type_problems, declarations_by_id: state.declarations_by_id, dep_idents, exposed_aliases: exposed_aliases_by_symbol, exposed_values, exposed_to_host: exposed_vars_by_symbol.into_iter().collect(), exposed_types_storage, resolved_implementations, sources, timings: state.timings, documentation, abilities_store, } } /// Load a `platform` module fn load_platform_module<'a>( arena: &'a Bump, filename: &Path, shorthand: &'a str, app_module_id: ModuleId, module_ids: Arc>>, ident_ids_by_module: SharedIdentIdsByModule, ) -> Result, LoadingProblem<'a>> { let module_start_time = Instant::now(); let file_io_start = Instant::now(); let file = fs::read(&filename); let file_io_duration = file_io_start.elapsed(); match file { Ok(bytes_vec) => { let parse_start = Instant::now(); let bytes = arena.alloc(bytes_vec); let parse_state = roc_parse::state::State::new(bytes); let parsed = roc_parse::module::parse_header(arena, parse_state.clone()); let parse_header_duration = parse_start.elapsed(); // Insert the first entries for this module's timings let mut pkg_module_timing = ModuleTiming::new(module_start_time); pkg_module_timing.read_roc_file = file_io_duration; pkg_module_timing.parse_header = parse_header_duration; match parsed { Ok((ast::Module::Interface { header }, _parse_state)) => { Err(LoadingProblem::UnexpectedHeader(format!( "expected platform/package module, got Interface with header\n{:?}", header ))) } Ok((ast::Module::Hosted { header }, _parse_state)) => { Err(LoadingProblem::UnexpectedHeader(format!( "expected platform/package module, got Hosted module with header\n{:?}", header ))) } Ok((ast::Module::App { header }, _parse_state)) => { Err(LoadingProblem::UnexpectedHeader(format!( "expected platform/package module, got App with header\n{:?}", header ))) } Ok((ast::Module::Platform { header }, parser_state)) => { // make a `platform` module that ultimately exposes `main` to the host let platform_module_msg = fabricate_platform_module( arena, Some(shorthand), Some(app_module_id), filename.to_path_buf(), parser_state, module_ids.clone(), ident_ids_by_module, &header, pkg_module_timing, ) .1; Ok(platform_module_msg) } Err(fail) => Err(LoadingProblem::ParsingFailed( fail.map_problem(SyntaxError::Header) .into_file_error(filename.to_path_buf()), )), } } Err(err) => Err(LoadingProblem::FileProblem { filename: filename.to_path_buf(), error: err.kind(), }), } } fn load_builtin_module_help<'a>( arena: &'a Bump, filename: &str, src_bytes: &'a str, ) -> (HeaderInfo<'a>, roc_parse::state::State<'a>) { let is_root_module = false; let opt_shorthand = None; let filename = PathBuf::from(filename); let parse_state = roc_parse::state::State::new(src_bytes.as_bytes()); let parsed = roc_parse::module::parse_header(arena, parse_state.clone()); match parsed { Ok((ast::Module::Interface { header }, parse_state)) => { let info = HeaderInfo { loc_name: Loc { region: header.name.region, value: ModuleNameEnum::Interface(header.name.value), }, filename, is_root_module, opt_shorthand, packages: &[], exposes: unspace(arena, header.exposes.items), imports: unspace(arena, header.imports.items), extra: HeaderFor::Builtin { generates_with: &[], }, }; (info, parse_state) } Ok(_) => panic!("invalid header format for builtin module"), Err(e) => panic!( "Hit a parse error in the header of {:?}:\n{:?}", filename, e ), } } fn load_builtin_module<'a>( arena: &'a Bump, module_ids: Arc>>, ident_ids_by_module: SharedIdentIdsByModule, module_timing: ModuleTiming, module_id: ModuleId, module_name: &str, ) -> (ModuleId, Msg<'a>) { let src_bytes = module_source(module_id); let (info, parse_state) = load_builtin_module_help(arena, module_name, src_bytes); send_header( info, parse_state, module_ids, ident_ids_by_module, module_timing, ) } /// Load a module by its module name, rather than by its filename fn load_module<'a>( arena: &'a Bump, src_dir: &Path, module_name: PQModuleName<'a>, module_ids: Arc>>, arc_shorthands: Arc>>>, ident_ids_by_module: SharedIdentIdsByModule, ) -> Result<(ModuleId, Msg<'a>), LoadingProblem<'a>> { let module_start_time = Instant::now(); let parse_start = Instant::now(); let parse_header_duration = parse_start.elapsed(); // Insert the first entries for this module's timings let mut module_timing = ModuleTiming::new(module_start_time); module_timing.read_roc_file = Default::default(); module_timing.parse_header = parse_header_duration; macro_rules! load_builtins { ($($name:literal, $module_id:path)*) => { match module_name.as_inner().as_str() { $( $name => { return Ok(load_builtin_module( arena, module_ids, ident_ids_by_module, module_timing, $module_id, concat!($name, ".roc") )); } )* _ => { /* fall through */ } }} } load_builtins! { "Result", ModuleId::RESULT "List", ModuleId::LIST "Str", ModuleId::STR "Dict", ModuleId::DICT "Set", ModuleId::SET "Num", ModuleId::NUM "Bool", ModuleId::BOOL "Box", ModuleId::BOX "Encode", ModuleId::ENCODE "Decode", ModuleId::DECODE "Json", ModuleId::JSON } let (filename, opt_shorthand) = module_name_to_path(src_dir, module_name, arc_shorthands); load_filename( arena, filename, false, opt_shorthand, module_ids, ident_ids_by_module, module_start_time, ) } fn module_name_to_path<'a>( src_dir: &Path, module_name: PQModuleName<'a>, arc_shorthands: Arc>>>, ) -> (PathBuf, Option<&'a str>) { let mut filename; let opt_shorthand; match module_name { PQModuleName::Unqualified(name) => { filename = src_dir.to_path_buf(); opt_shorthand = None; // Convert dots in module name to directories for part in name.split(MODULE_SEPARATOR) { filename.push(part); } } PQModuleName::Qualified(shorthand, name) => { opt_shorthand = Some(shorthand); let shorthands = arc_shorthands.lock(); match shorthands.get(shorthand) { Some(path) => { let parent = Path::new(path.as_str()).parent().unwrap_or_else(|| { panic!( "platform module {:?} did not have a parent directory.", path ) }); filename = src_dir.join(parent) } None => unreachable!("there is no shorthand named {:?}", shorthand), } // Convert dots in module name to directories for part in name.split(MODULE_SEPARATOR) { filename.push(part); } } } // End with .roc filename.set_extension(ROC_FILE_EXTENSION); (filename, opt_shorthand) } /// Find a task according to the following algorithm: /// /// 1. Look in a local Worker queue. If it has a task, pop it off the queue and return it. /// 2. If that queue was empty, ask the global queue for a task. /// 3. If the global queue is also empty, iterate through each Stealer (each Worker queue has a /// corresponding Stealer, which can steal from it. Stealers can be shared across threads.) /// /// Based on https://docs.rs/crossbeam/0.7.3/crossbeam/deque/index.html#examples fn find_task(local: &Worker, global: &Injector, stealers: &[Stealer]) -> Option { // Pop a task from the local queue, if not empty. local.pop().or_else(|| { // Otherwise, we need to look for a task elsewhere. iter::repeat_with(|| { // Try stealing a task from the global queue. global .steal() // Or try stealing a task from one of the other threads. .or_else(|| stealers.iter().map(|s| s.steal()).collect()) }) // Loop while no task was stolen and any steal operation needs to be retried. .find(|s| !s.is_retry()) // Extract the stolen task, if there is one. .and_then(|s| s.success()) }) } #[allow(clippy::too_many_arguments)] fn parse_header<'a>( arena: &'a Bump, read_file_duration: Duration, filename: PathBuf, is_root_module: bool, opt_shorthand: Option<&'a str>, module_ids: Arc>>, ident_ids_by_module: SharedIdentIdsByModule, src_bytes: &'a [u8], start_time: Instant, ) -> Result<(ModuleId, Msg<'a>), LoadingProblem<'a>> { let parse_start = Instant::now(); let parse_state = roc_parse::state::State::new(src_bytes); let parsed = roc_parse::module::parse_header(arena, parse_state.clone()); let parse_header_duration = parse_start.elapsed(); // Insert the first entries for this module's timings let mut module_timing = ModuleTiming::new(start_time); module_timing.read_roc_file = read_file_duration; module_timing.parse_header = parse_header_duration; match parsed { Ok((ast::Module::Interface { header }, parse_state)) => { let info = HeaderInfo { loc_name: Loc { region: header.name.region, value: ModuleNameEnum::Interface(header.name.value), }, filename, is_root_module, opt_shorthand, packages: &[], exposes: unspace(arena, header.exposes.items), imports: unspace(arena, header.imports.items), extra: HeaderFor::Interface, }; Ok(send_header( info, parse_state, module_ids, ident_ids_by_module, module_timing, )) } Ok((ast::Module::Hosted { header }, parse_state)) => { let info = HeaderInfo { loc_name: Loc { region: header.name.region, value: ModuleNameEnum::Hosted(header.name.value), }, filename, is_root_module, opt_shorthand, packages: &[], exposes: unspace(arena, header.exposes.items), imports: unspace(arena, header.imports.items), extra: HeaderFor::Hosted { generates: header.generates, generates_with: unspace(arena, header.generates_with.items), }, }; Ok(send_header( info, parse_state, module_ids, ident_ids_by_module, module_timing, )) } Ok((ast::Module::App { header }, parse_state)) => { let mut app_file_dir = filename.clone(); app_file_dir.pop(); let packages = unspace(arena, header.packages.items); let mut exposes = bumpalo::collections::Vec::new_in(arena); exposes.extend(unspace(arena, header.provides.items)); if let Some(provided_types) = header.provides_types { for provided_type in unspace(arena, provided_types.items) { let string: &str = provided_type.value.into(); let exposed_name = ExposedName::new(string); exposes.push(Loc::at(provided_type.region, exposed_name)); } } let exposes = exposes.into_bump_slice(); let info = HeaderInfo { loc_name: Loc { region: header.name.region, value: ModuleNameEnum::App(header.name.value), }, filename, is_root_module, opt_shorthand, packages, exposes, imports: unspace(arena, header.imports.items), extra: HeaderFor::App { to_platform: header.to.value, }, }; let (module_id, app_module_header_msg) = send_header( info, parse_state, module_ids.clone(), ident_ids_by_module.clone(), module_timing, ); match header.to.value { To::ExistingPackage(existing_package) => { let opt_base_package = packages.iter().find_map(|loc_package_entry| { let Loc { value, .. } = loc_package_entry; if value.shorthand == existing_package { Some(value) } else { None } }); if let Some(PackageEntry { shorthand, package_name: Loc { value: package_name, .. }, .. }) = opt_base_package { // check whether we can find a `platform` module file let platform_module_path = app_file_dir.join(package_name.to_str()); if platform_module_path.as_path().exists() { let load_platform_module_msg = load_platform_module( arena, &platform_module_path, shorthand, module_id, module_ids, ident_ids_by_module, )?; Ok(( module_id, Msg::Many(vec![app_module_header_msg, load_platform_module_msg]), )) } else { Err(LoadingProblem::FileProblem { filename: platform_module_path, error: io::ErrorKind::NotFound, }) } } else { panic!("could not find base") } } To::NewPackage(_package_name) => Ok((module_id, app_module_header_msg)), } } Ok((ast::Module::Platform { header }, parse_state)) => Ok(fabricate_platform_module( arena, None, None, filename, parse_state, module_ids.clone(), ident_ids_by_module, &header, module_timing, )), Err(fail) => Err(LoadingProblem::ParsingFailed( fail.map_problem(SyntaxError::Header) .into_file_error(filename), )), } } /// Load a module by its filename #[allow(clippy::too_many_arguments)] fn load_filename<'a>( arena: &'a Bump, filename: PathBuf, is_root_module: bool, opt_shorthand: Option<&'a str>, module_ids: Arc>>, ident_ids_by_module: SharedIdentIdsByModule, module_start_time: Instant, ) -> Result<(ModuleId, Msg<'a>), LoadingProblem<'a>> { let file_io_start = Instant::now(); let file = fs::read(&filename); let file_io_duration = file_io_start.elapsed(); match file { Ok(bytes) => parse_header( arena, file_io_duration, filename, is_root_module, opt_shorthand, module_ids, ident_ids_by_module, arena.alloc(bytes), module_start_time, ), Err(err) => Err(LoadingProblem::FileProblem { filename, error: err.kind(), }), } } /// Load a module from a str /// the `filename` is never read, but used for the module name #[allow(clippy::too_many_arguments)] fn load_from_str<'a>( arena: &'a Bump, filename: PathBuf, src: &'a str, module_ids: Arc>>, ident_ids_by_module: SharedIdentIdsByModule, module_start_time: Instant, ) -> Result<(ModuleId, Msg<'a>), LoadingProblem<'a>> { let file_io_start = Instant::now(); let file_io_duration = file_io_start.elapsed(); parse_header( arena, file_io_duration, filename, false, None, module_ids, ident_ids_by_module, src.as_bytes(), module_start_time, ) } #[derive(Debug)] struct HeaderInfo<'a> { loc_name: Loc>, filename: PathBuf, is_root_module: bool, opt_shorthand: Option<&'a str>, packages: &'a [Loc>], exposes: &'a [Loc>], imports: &'a [Loc>], extra: HeaderFor<'a>, } #[allow(clippy::too_many_arguments)] fn send_header<'a>( info: HeaderInfo<'a>, parse_state: roc_parse::state::State<'a>, module_ids: Arc>>, ident_ids_by_module: SharedIdentIdsByModule, module_timing: ModuleTiming, ) -> (ModuleId, Msg<'a>) { use ModuleNameEnum::*; let HeaderInfo { loc_name, filename, is_root_module, opt_shorthand, packages, exposes, imports, extra, } = info; let declared_name: ModuleName = match &loc_name.value { Platform => unreachable!(), App(_) => ModuleName::APP.into(), Interface(module_name) | Hosted(module_name) => { // TODO check to see if module_name is consistent with filename. // If it isn't, report a problem! module_name.as_str().into() } }; let mut imported: Vec<(QualifiedModuleName, Vec, Region)> = Vec::with_capacity(imports.len()); let mut imported_modules: MutMap = MutMap::default(); let mut scope_size = 0; for loc_entry in imports { let (qualified_module_name, exposed) = exposed_from_import(&loc_entry.value); scope_size += exposed.len(); imported.push((qualified_module_name, exposed, loc_entry.region)); } let num_exposes = exposes.len(); let mut deps_by_name: MutMap = HashMap::with_capacity_and_hasher(num_exposes, default_hasher()); let mut exposed: Vec = Vec::with_capacity(num_exposes); // Make sure the module_ids has ModuleIds for all our deps, // then record those ModuleIds in can_module_ids for later. let mut scope: MutMap = HashMap::with_capacity_and_hasher(scope_size, default_hasher()); let home: ModuleId; let ident_ids = { // Lock just long enough to perform the minimal operations necessary. let mut module_ids = (*module_ids).lock(); let mut ident_ids_by_module = (*ident_ids_by_module).lock(); let name = match opt_shorthand { Some(shorthand) => PQModuleName::Qualified(shorthand, declared_name), None => PQModuleName::Unqualified(declared_name), }; home = module_ids.get_or_insert(&name); // Ensure this module has an entry in the exposed_ident_ids map. ident_ids_by_module.get_or_insert(home); // For each of our imports, add an entry to deps_by_name // // e.g. for `imports [pf.Foo.{ bar }]`, add `Foo` to deps_by_name // // Also build a list of imported_values_to_expose (like `bar` above.) for (qualified_module_name, exposed_idents, region) in imported.into_iter() { let cloned_module_name = qualified_module_name.module.clone(); let pq_module_name = match qualified_module_name.opt_package { None => match opt_shorthand { Some(shorthand) => { PQModuleName::Qualified(shorthand, qualified_module_name.module) } None => PQModuleName::Unqualified(qualified_module_name.module), }, Some(package) => PQModuleName::Qualified(package, cloned_module_name), }; let module_id = module_ids.get_or_insert(&pq_module_name); imported_modules.insert(module_id, region); deps_by_name.insert(pq_module_name, module_id); // Add the new exposed idents to the dep module's IdentIds, so // once that module later gets loaded, its lookups will resolve // to the same symbols as the ones we're using here. let ident_ids = ident_ids_by_module.get_or_insert(module_id); for ident in exposed_idents { let ident_id = ident_ids.get_or_insert(ident.as_str()); let symbol = Symbol::new(module_id, ident_id); // Since this value is exposed, add it to our module's default scope. debug_assert!(!scope.contains_key(&ident)); scope.insert(ident, (symbol, region)); } } let ident_ids = ident_ids_by_module.get_mut(&home).unwrap(); // Generate IdentIds entries for all values this module exposes. // This way, when we encounter them in Defs later, they already // have an IdentIds entry. // // We must *not* add them to scope yet, or else the Defs will // incorrectly think they're shadowing them! for loc_exposed in exposes.iter() { // Use get_or_insert here because the ident_ids may already // created an IdentId for this, when it was imported exposed // in a dependent module. // // For example, if module A has [B.{ foo }], then // when we get here for B, `foo` will already have // an IdentId. We must reuse that! let ident_id = ident_ids.get_or_insert(loc_exposed.value.as_str()); let symbol = Symbol::new(home, ident_id); exposed.push(symbol); } if cfg!(debug_assertions) { home.register_debug_idents(ident_ids); } ident_ids.clone() }; let package_entries = packages .iter() .map(|pkg| { let pkg = pkg.value; (pkg.shorthand, pkg.package_name.value) }) .collect::>(); // Send the deps to the coordinator thread for processing, // then continue on to parsing and canonicalizing defs. // // We always need to send these, even if deps is empty, // because the coordinator thread needs to receive this message // to decrement its "pending" count. let mut package_qualified_imported_modules = MutSet::default(); for (pq_module_name, module_id) in &deps_by_name { match pq_module_name { PackageQualified::Unqualified(_) => { package_qualified_imported_modules .insert(PackageQualified::Unqualified(*module_id)); } PackageQualified::Qualified(shorthand, _) => { package_qualified_imported_modules .insert(PackageQualified::Qualified(shorthand, *module_id)); } } } // make sure when we run the bulitin modules in /compiler/builtins/roc that we // mark these modules as Builtin. Otherwise the builtin functions are not instantiated // and we just have a bunch of definitions with runtime errors in their bodies let extra = { match extra { HeaderFor::Interface if home.is_builtin() => HeaderFor::Builtin { generates_with: &[], }, _ => extra, } }; ( home, Msg::Header(ModuleHeader { module_id: home, module_path: filename, is_root_module, exposed_ident_ids: ident_ids, module_name: loc_name.value, packages: package_entries, imported_modules, package_qualified_imported_modules, deps_by_name, exposes: exposed, parse_state, exposed_imports: scope, symbols_from_requires: Vec::new(), header_for: extra, module_timing, }), ) } #[derive(Debug)] struct PlatformHeaderInfo<'a> { filename: PathBuf, is_root_module: bool, opt_shorthand: Option<&'a str>, opt_app_module_id: Option, packages: &'a [Loc>], provides: &'a [Loc>], requires: &'a [Loc>], requires_types: &'a [Loc>], imports: &'a [Loc>], } // TODO refactor so more logic is shared with `send_header` #[allow(clippy::too_many_arguments)] fn send_header_two<'a>( info: PlatformHeaderInfo<'a>, parse_state: roc_parse::state::State<'a>, module_ids: Arc>>, ident_ids_by_module: SharedIdentIdsByModule, module_timing: ModuleTiming, ) -> (ModuleId, Msg<'a>) { let PlatformHeaderInfo { filename, opt_shorthand, is_root_module, opt_app_module_id, packages, provides, requires, requires_types, imports, } = info; let declared_name: ModuleName = "".into(); let mut symbols_from_requires = Vec::with_capacity(requires.len()); let mut imported: Vec<(QualifiedModuleName, Vec, Region)> = Vec::with_capacity(imports.len()); let mut imported_modules: MutMap = MutMap::default(); let num_exposes = provides.len(); let mut deps_by_name: MutMap = HashMap::with_capacity_and_hasher(num_exposes, default_hasher()); // Add standard imports, if there is an app module. // (There might not be, e.g. when running `roc check myplatform.roc` or // when generating bindings.) if let Some(app_module_id) = opt_app_module_id { imported_modules.insert(app_module_id, Region::zero()); deps_by_name.insert( PQModuleName::Unqualified(ModuleName::APP.into()), app_module_id, ); } let mut scope_size = 0; for loc_entry in imports { let (qualified_module_name, exposed) = exposed_from_import(&loc_entry.value); scope_size += exposed.len(); imported.push((qualified_module_name, exposed, loc_entry.region)); } let mut exposed: Vec = Vec::with_capacity(num_exposes); // Make sure the module_ids has ModuleIds for all our deps, // then record those ModuleIds in can_module_ids for later. let mut scope: MutMap = HashMap::with_capacity_and_hasher(scope_size, default_hasher()); let home: ModuleId; let mut ident_ids = { // Lock just long enough to perform the minimal operations necessary. let mut module_ids = (*module_ids).lock(); let mut ident_ids_by_module = (*ident_ids_by_module).lock(); let name = match opt_shorthand { Some(shorthand) => PQModuleName::Qualified(shorthand, declared_name), None => PQModuleName::Unqualified(declared_name), }; home = module_ids.get_or_insert(&name); // Ensure this module has an entry in the exposed_ident_ids map. ident_ids_by_module.get_or_insert(home); // For each of our imports, add an entry to deps_by_name // // e.g. for `imports [pf.Foo.{ bar }]`, add `Foo` to deps_by_name // // Also build a list of imported_values_to_expose (like `bar` above.) for (qualified_module_name, exposed_idents, region) in imported.into_iter() { let cloned_module_name = qualified_module_name.module.clone(); let pq_module_name = match qualified_module_name.opt_package { None => match opt_shorthand { Some(shorthand) => { PQModuleName::Qualified(shorthand, qualified_module_name.module) } None => PQModuleName::Unqualified(qualified_module_name.module), }, Some(package) => PQModuleName::Qualified(package, cloned_module_name), }; let module_id = module_ids.get_or_insert(&pq_module_name); imported_modules.insert(module_id, region); deps_by_name.insert(pq_module_name, module_id); // Add the new exposed idents to the dep module's IdentIds, so // once that module later gets loaded, its lookups will resolve // to the same symbols as the ones we're using here. let ident_ids = ident_ids_by_module.get_or_insert(module_id); for ident in exposed_idents { let ident_id = ident_ids.get_or_insert(ident.as_str()); let symbol = Symbol::new(module_id, ident_id); // Since this value is exposed, add it to our module's default scope. debug_assert!(!scope.contains_key(&ident.clone())); scope.insert(ident, (symbol, region)); } } { // If we don't have an app module id (e.g. because we're doing // `roc check myplatform.roc` or because we're generating glue code), // insert the `requires` symbols into the platform module's IdentIds. // // Otherwise, get them from the app module's IdentIds, because it // should already have a symbol for each `requires` entry, and we // want to make sure we're referencing the same symbols! let module_id = opt_app_module_id.unwrap_or(home); let ident_ids = ident_ids_by_module.get_or_insert(module_id); for entry in requires { let entry = entry.value; let ident: Ident = entry.ident.value.into(); let ident_id = ident_ids.get_or_insert(entry.ident.value); let symbol = Symbol::new(module_id, ident_id); // Since this value is exposed, add it to our module's default scope. debug_assert!(!scope.contains_key(&ident.clone())); scope.insert(ident, (symbol, entry.ident.region)); symbols_from_requires.push((Loc::at(entry.ident.region, symbol), entry.ann)); } for entry in requires_types { let string: &str = entry.value.into(); let ident: Ident = string.into(); let ident_id = ident_ids.get_or_insert(string); let symbol = Symbol::new(module_id, ident_id); // Since this value is exposed, add it to our module's default scope. debug_assert!(!scope.contains_key(&ident)); scope.insert(ident, (symbol, entry.region)); } } let ident_ids = ident_ids_by_module.get_mut(&home).unwrap(); // Generate IdentIds entries for all values this module exposes. // This way, when we encounter them in Defs later, they already // have an IdentIds entry. // // We must *not* add them to scope yet, or else the Defs will // incorrectly think they're shadowing them! for loc_exposed in provides.iter() { // Use get_or_insert here because the ident_ids may already // created an IdentId for this, when it was imported exposed // in a dependent module. // // For example, if module A has [B.{ foo }], then // when we get here for B, `foo` will already have // an IdentId. We must reuse that! let ident_id = ident_ids.get_or_insert(loc_exposed.value.as_str()); let symbol = Symbol::new(home, ident_id); exposed.push(symbol); } if cfg!(debug_assertions) { home.register_debug_idents(ident_ids); } ident_ids.clone() }; let package_entries = packages .iter() .map(|pkg| (pkg.value.shorthand, pkg.value.package_name.value)) .collect::>(); // Send the deps to the coordinator thread for processing, // then continue on to parsing and canonicalizing defs. // // We always need to send these, even if deps is empty, // because the coordinator thread needs to receive this message // to decrement its "pending" count. let module_name = ModuleNameEnum::Platform; let main_for_host = { let ident_id = ident_ids.get_or_insert(provides[0].value.as_str()); Symbol::new(home, ident_id) }; let extra = HeaderFor::Platform { // A config_shorthand of "" should be fine config_shorthand: opt_shorthand.unwrap_or_default(), platform_main_type: requires[0].value, main_for_host, }; let mut package_qualified_imported_modules = MutSet::default(); for (pq_module_name, module_id) in &deps_by_name { match pq_module_name { PackageQualified::Unqualified(_) => { package_qualified_imported_modules .insert(PackageQualified::Unqualified(*module_id)); } PackageQualified::Qualified(shorthand, _) => { package_qualified_imported_modules .insert(PackageQualified::Qualified(shorthand, *module_id)); } } } ( home, Msg::Header(ModuleHeader { module_id: home, module_path: filename, is_root_module, exposed_ident_ids: ident_ids, module_name, packages: package_entries, imported_modules, package_qualified_imported_modules, deps_by_name, exposes: exposed, parse_state, exposed_imports: scope, module_timing, symbols_from_requires, header_for: extra, }), ) } impl<'a> BuildTask<'a> { // TODO trim down these arguments - possibly by moving Constraint into Module #[allow(clippy::too_many_arguments)] fn solve_module( module: Module, ident_ids: IdentIds, module_timing: ModuleTiming, constraints: Constraints, constraint: ConstraintSoa, pending_derives: PendingDerives, var_store: VarStore, imported_modules: MutMap, exposed_types: &ExposedByModule, dep_idents: IdentIdsByModule, declarations: Declarations, cached_subs: CachedSubs, derived_module: SharedDerivedModule, ) -> Self { let exposed_by_module = exposed_types.retain_modules(imported_modules.keys()); let exposed_for_module = ExposedForModule::new(module.referenced_values.iter(), exposed_by_module); // Next, solve this module in the background. Self::Solve { module, ident_ids, exposed_for_module, constraints, constraint, pending_derives, var_store, declarations, dep_idents, module_timing, cached_subs, derived_module, } } } fn synth_import(subs: &mut Subs, content: roc_types::subs::Content) -> Variable { use roc_types::subs::{Descriptor, Mark, OptVariable, Rank}; subs.fresh(Descriptor { content, rank: Rank::import(), mark: Mark::NONE, copy: OptVariable::NONE, }) } fn synth_list_len_type(subs: &mut Subs) -> Variable { use roc_types::subs::{Content, FlatType, LambdaSet, OptVariable, SubsSlice, UnionLabels}; // List.len : List a -> Nat let a = synth_import(subs, Content::FlexVar(None)); let a_slice = SubsSlice::extend_new(&mut subs.variables, [a]); let list_a = synth_import( subs, Content::Structure(FlatType::Apply(Symbol::LIST_LIST, a_slice)), ); let fn_var = synth_import(subs, Content::Error); let solved_list_len = UnionLabels::insert_into_subs(subs, [(Symbol::LIST_LEN, [])]); let clos_list_len = synth_import( subs, Content::LambdaSet(LambdaSet { solved: solved_list_len, recursion_var: OptVariable::NONE, unspecialized: SubsSlice::default(), ambient_function: fn_var, }), ); let fn_args_slice = SubsSlice::extend_new(&mut subs.variables, [list_a]); subs.set_content( fn_var, Content::Structure(FlatType::Func(fn_args_slice, clos_list_len, Variable::NAT)), ); fn_var } pub fn add_imports( my_module: ModuleId, subs: &mut Subs, mut pending_abilities: PendingAbilitiesStore, exposed_for_module: &ExposedForModule, def_types: &mut Vec<(Symbol, Loc)>, rigid_vars: &mut Vec, ) -> (Vec, AbilitiesStore) { use roc_types::types::Type; let mut import_variables = Vec::new(); let mut cached_symbol_vars = VecMap::default(); macro_rules! import_var_for_symbol { ($subs:expr, $exposed_by_module:expr, $symbol:ident, $break:stmt) => { let module_id = $symbol.module_id(); match $exposed_by_module.get(&module_id) { Some(ExposedModuleTypes { exposed_types_storage_subs: exposed_types, resolved_implementations: _, }) => { let variable = match exposed_types.stored_vars_by_symbol.iter().find(|(s, _)| **s == $symbol) { None => { // Today we define builtins in each module that uses them // so even though they have a different module name from // the surrounding module, they are not technically imported debug_assert!($symbol.is_builtin()); $break } Some((_, x)) => *x, }; let copied_import = exposed_types.storage_subs.export_variable_to($subs, variable); def_types.push(( $symbol, Loc::at_zero(Type::Variable(copied_import.variable)), )); // not a typo; rigids are turned into flex during type inference, but when imported we must // consider them rigid variables rigid_vars.extend(copied_import.rigid); rigid_vars.extend(copied_import.flex); // Rigid vars bound to abilities are also treated like rigids. rigid_vars.extend(copied_import.rigid_able); rigid_vars.extend(copied_import.flex_able); import_variables.extend(copied_import.registered); cached_symbol_vars.insert($symbol, copied_import.variable); } None => { internal_error!("Imported module {:?} is not available", module_id) } } } } for &symbol in &exposed_for_module.imported_values { import_var_for_symbol!(subs, exposed_for_module.exposed_by_module, symbol, continue); } // Patch used symbols from circular dependencies. if my_module == ModuleId::NUM { // Num needs List.len, but List imports Num. let list_len_type = synth_list_len_type(subs); def_types.push(( Symbol::LIST_LEN, Loc::at_zero(Type::Variable(list_len_type)), )); import_variables.push(list_len_type); } // TODO: see if we can reduce the amount of specializations we need to import. // One idea is to just always assume external modules fulfill their specialization obligations // and save lambda set resolution for mono. for (_, module_types) in exposed_for_module.exposed_by_module.iter_all() { for (impl_key, resolved_impl) in module_types.resolved_implementations.iter() { pending_abilities.import_implementation(*impl_key, resolved_impl); } } struct Ctx<'a> { subs: &'a mut Subs, exposed_by_module: &'a ExposedByModule, } let abilities_store = pending_abilities.resolve_for_module( my_module, &mut Ctx { subs, exposed_by_module: &exposed_for_module.exposed_by_module, }, |ctx, symbol| match cached_symbol_vars.get(&symbol).copied() { Some(var) => var, None => { import_var_for_symbol!( ctx.subs, ctx.exposed_by_module, symbol, internal_error!("Import ability member {:?} not available", symbol) ); *cached_symbol_vars.get(&symbol).unwrap() } }, |ctx, module, lset_var| match ctx.exposed_by_module.get(&module) { Some(ExposedModuleTypes { exposed_types_storage_subs: exposed_types, resolved_implementations: _, }) => { let var = exposed_types .stored_specialization_lambda_set_vars .get(&lset_var) .expect("Lambda set var from other module not available"); let copied_import = exposed_types .storage_subs .export_variable_to(ctx.subs, *var); copied_import.variable } None => internal_error!("Imported module {:?} is not available", module), }, ); (import_variables, abilities_store) } #[allow(clippy::complexity)] fn run_solve_solve( exposed_for_module: ExposedForModule, mut constraints: Constraints, constraint: ConstraintSoa, pending_derives: PendingDerives, var_store: VarStore, module: Module, derived_module: SharedDerivedModule, ) -> ( Solved, ResolvedImplementations, Vec<(Symbol, Variable)>, Vec, AbilitiesStore, ) { let Module { exposed_symbols, aliases, rigid_variables, abilities_store: pending_abilities, .. } = module; let mut rigid_vars: Vec = Vec::new(); let mut def_types: Vec<(Symbol, Loc)> = Vec::new(); let mut subs = Subs::new_from_varstore(var_store); let (import_variables, abilities_store) = add_imports( module.module_id, &mut subs, pending_abilities, &exposed_for_module, &mut def_types, &mut rigid_vars, ); let actual_constraint = constraints.let_import_constraint(rigid_vars, def_types, constraint, &import_variables); let mut solve_aliases = default_aliases(); for (name, (_, alias)) in aliases.iter() { solve_aliases.insert(*name, alias.clone()); } let (solved_subs, solved_implementations, exposed_vars_by_symbol, problems, abilities_store) = { let module_id = module.module_id; let (solved_subs, solved_env, problems, abilities_store) = roc_solve::module::run_solve( module_id, &constraints, actual_constraint, rigid_variables, subs, solve_aliases, abilities_store, pending_derives, &exposed_for_module.exposed_by_module, derived_module, ); let solved_implementations = extract_module_owned_implementations(module_id, &abilities_store); let is_specialization_symbol = |sym| { solved_implementations .values() .any(|resolved_impl| match resolved_impl { ResolvedImpl::Impl(specialization) => specialization.symbol == sym, ResolvedImpl::Derived | ResolvedImpl::Error => false, }) }; // Expose anything that is explicitly exposed by the header, or is a specialization of an // ability. let exposed_vars_by_symbol: Vec<_> = solved_env .vars_by_symbol() .filter(|(k, _)| exposed_symbols.contains(k) || is_specialization_symbol(*k)) .collect(); ( solved_subs, solved_implementations, exposed_vars_by_symbol, problems, abilities_store, ) }; ( solved_subs, solved_implementations, exposed_vars_by_symbol, problems, abilities_store, ) } #[allow(clippy::too_many_arguments)] fn run_solve<'a>( module: Module, ident_ids: IdentIds, mut module_timing: ModuleTiming, exposed_for_module: ExposedForModule, constraints: Constraints, constraint: ConstraintSoa, pending_derives: PendingDerives, var_store: VarStore, decls: Declarations, dep_idents: IdentIdsByModule, cached_subs: CachedSubs, derived_module: SharedDerivedModule, ) -> Msg<'a> { let solve_start = Instant::now(); let module_id = module.module_id; // TODO remove when we write builtins in roc let aliases = module.aliases.clone(); let mut module = module; let loc_expects = std::mem::take(&mut module.loc_expects); let module = module; let (solved_subs, solved_implementations, exposed_vars_by_symbol, problems, abilities_store) = { if module_id.is_builtin() { match cached_subs.lock().remove(&module_id) { None => run_solve_solve( exposed_for_module, constraints, constraint, pending_derives, var_store, module, derived_module, ), Some((subs, exposed_vars_by_symbol)) => { ( Solved(subs), // TODO(abilities) cache abilities for builtins VecMap::default(), exposed_vars_by_symbol.to_vec(), vec![], // TODO(abilities) cache abilities for builtins AbilitiesStore::default(), ) } } } else { run_solve_solve( exposed_for_module, constraints, constraint, pending_derives, var_store, module, derived_module, ) } }; let mut solved_subs = solved_subs; let exposed_types = roc_solve::module::exposed_types_storage_subs( module_id, &mut solved_subs, &exposed_vars_by_symbol, &solved_implementations, &abilities_store, ); let solved_module = SolvedModule { exposed_vars_by_symbol, problems, aliases, solved_implementations, exposed_types, }; // Record the final timings let solve_end = Instant::now(); module_timing.solve = solve_end.duration_since(solve_start); // Send the subs to the main thread for processing, Msg::SolvedTypes { module_id, solved_subs, ident_ids, decls, dep_idents, solved_module, module_timing, abilities_store, loc_expects, } } fn unspace<'a, T: Copy>(arena: &'a Bump, items: &[Loc>]) -> &'a [Loc] { bumpalo::collections::Vec::from_iter_in( items .iter() .map(|item| Loc::at(item.region, item.value.extract_spaces().item)), arena, ) .into_bump_slice() } #[allow(clippy::too_many_arguments)] fn fabricate_platform_module<'a>( arena: &'a Bump, opt_shorthand: Option<&'a str>, opt_app_module_id: Option, filename: PathBuf, parse_state: roc_parse::state::State<'a>, module_ids: Arc>>, ident_ids_by_module: SharedIdentIdsByModule, header: &PlatformHeader<'a>, module_timing: ModuleTiming, ) -> (ModuleId, Msg<'a>) { // If we have an app module, then it's the root module; // otherwise, we must be the root. let is_root_module = opt_app_module_id.is_none(); let info = PlatformHeaderInfo { filename, is_root_module, opt_shorthand, opt_app_module_id, packages: &[], provides: unspace(arena, header.provides.items), requires: &*arena.alloc([Loc::at( header.requires.signature.region, header.requires.signature.extract_spaces().item, )]), requires_types: unspace(arena, header.requires.rigids.items), imports: unspace(arena, header.imports.items), }; send_header_two( info, parse_state, module_ids, ident_ids_by_module, module_timing, ) } #[allow(clippy::too_many_arguments)] #[allow(clippy::unnecessary_wraps)] fn canonicalize_and_constrain<'a>( arena: &'a Bump, module_ids: &ModuleIds, dep_idents: IdentIdsByModule, exposed_symbols: VecSet, aliases: MutMap, imported_abilities_state: PendingAbilitiesStore, parsed: ParsedModule<'a>, skip_constraint_gen: bool, ) -> CanAndCon { let canonicalize_start = Instant::now(); let ParsedModule { module_id, module_name, header_for, exposed_ident_ids, parsed_defs, exposed_imports, imported_modules, mut module_timing, symbols_from_requires, .. } = parsed; // _before has an underscore because it's unused in --release builds let _before = roc_types::types::get_type_clone_count(); let parsed_defs_for_docs = parsed_defs.clone(); let parsed_defs = arena.alloc(parsed_defs); let mut var_store = VarStore::default(); let module_output = canonicalize_module_defs( arena, parsed_defs, &header_for, module_id, module_ids, exposed_ident_ids, &dep_idents, aliases, imported_abilities_state, exposed_imports, &exposed_symbols, &symbols_from_requires, &mut var_store, ); // _after has an underscore because it's unused in --release builds let _after = roc_types::types::get_type_clone_count(); log!( "canonicalize of {:?} cloned Type {} times ({} -> {})", module_id, _after - _before, _before, _after ); let canonicalize_end = Instant::now(); module_timing.canonicalize = canonicalize_end.duration_since(canonicalize_start); // Generate documentation information // TODO: store timing information? let module_docs = match module_name { ModuleNameEnum::Platform => None, ModuleNameEnum::App(_) => None, ModuleNameEnum::Interface(name) | ModuleNameEnum::Hosted(name) => { let mut scope = module_output.scope.clone(); scope.add_docs_imports(); let docs = crate::docs::generate_module_docs( scope, name.as_str().into(), &parsed_defs_for_docs, ); Some(docs) } }; // _before has an underscore because it's unused in --release builds let _before = roc_types::types::get_type_clone_count(); let mut constraints = Constraints::new(); let constraint = if skip_constraint_gen { roc_can::constraint::Constraint::True } else { constrain_module( &mut constraints, module_output.symbols_from_requires, &module_output.scope.abilities_store, &module_output.declarations, module_id, ) }; // _after has an underscore because it's unused in --release builds let _after = roc_types::types::get_type_clone_count(); log!( "constraint gen of {:?} cloned Type {} times ({} -> {})", module_id, _after - _before, _before, _after ); // scope has imported aliases, but misses aliases from inner scopes // module_output.aliases does have those aliases, so we combine them let mut aliases: MutMap = module_output .aliases .into_iter() .map(|(k, v)| (k, (true, v))) .collect(); for (name, alias) in module_output.scope.aliases { match aliases.entry(name) { Occupied(_) => { // do nothing } Vacant(vacant) => { let should_include_builtin = matches!( name.module_id(), ModuleId::ENCODE | ModuleId::DECODE | ModuleId::DICT | ModuleId::SET ); if !name.is_builtin() || should_include_builtin { vacant.insert((false, alias)); } } } } let module = Module { module_id, exposed_imports: module_output.exposed_imports, exposed_symbols, referenced_values: module_output.referenced_values, referenced_types: module_output.referenced_types, aliases, rigid_variables: module_output.rigid_variables, abilities_store: module_output.scope.abilities_store, loc_expects: module_output.loc_expects, }; let constrained_module = ConstrainedModule { module, declarations: module_output.declarations, imported_modules, var_store, constraints, constraint, ident_ids: module_output.scope.locals.ident_ids, dep_idents, module_timing, pending_derives: module_output.pending_derives, }; CanAndCon { constrained_module, canonicalization_problems: module_output.problems, module_docs, } } fn parse<'a>(arena: &'a Bump, header: ModuleHeader<'a>) -> Result, LoadingProblem<'a>> { let mut module_timing = header.module_timing; let parse_start = Instant::now(); let source = header.parse_state.original_bytes(); let parse_state = header.parse_state; let parsed_defs = match module_defs().parse(arena, parse_state) { Ok((_, success, _state)) => success, Err((_, fail, state)) => { return Err(LoadingProblem::ParsingFailed( fail.into_file_error(header.module_path, &state), )); } }; // Record the parse end time once, to avoid checking the time a second time // immediately afterward (for the beginning of canonicalization). let parse_end = Instant::now(); module_timing.parse_body = parse_end.duration_since(parse_start); let imported_modules = header.imported_modules; // SAFETY: By this point we've already incrementally verified that there // are no UTF-8 errors in these bytes. If there had been any UTF-8 errors, // we'd have bailed out before now. let src = unsafe { from_utf8_unchecked(source) }; let ModuleHeader { module_id, module_name, deps_by_name, exposed_ident_ids, exposed_imports, module_path, header_for, symbols_from_requires, .. } = header; let parsed = ParsedModule { module_id, module_name, module_path, src, module_timing, deps_by_name, imported_modules, exposed_ident_ids, exposed_imports, parsed_defs, symbols_from_requires, header_for, }; Ok(Msg::Parsed(parsed)) } fn exposed_from_import<'a>(entry: &ImportsEntry<'a>) -> (QualifiedModuleName<'a>, Vec) { use roc_parse::header::ImportsEntry::*; match entry { Module(module_name, exposes) => { let mut exposed = Vec::with_capacity(exposes.len()); for loc_entry in exposes.iter() { exposed.push(ident_from_exposed(&loc_entry.value)); } let qualified_module_name = QualifiedModuleName { opt_package: None, module: module_name.as_str().into(), }; (qualified_module_name, exposed) } Package(package_name, module_name, exposes) => { let mut exposed = Vec::with_capacity(exposes.len()); for loc_entry in exposes.iter() { exposed.push(ident_from_exposed(&loc_entry.value)); } let qualified_module_name = QualifiedModuleName { opt_package: Some(package_name), module: module_name.as_str().into(), }; (qualified_module_name, exposed) } } } fn ident_from_exposed(entry: &Spaced<'_, ExposedName<'_>>) -> Ident { entry.extract_spaces().item.as_str().into() } #[allow(clippy::too_many_arguments)] fn make_specializations<'a>( arena: &'a Bump, home: ModuleId, mut ident_ids: IdentIds, mut subs: Subs, procs_base: ProcsBase<'a>, mut layout_cache: LayoutCache<'a>, specializations_we_must_make: Vec>, mut module_timing: ModuleTiming, target_info: TargetInfo, world_abilities: WorldAbilities, exposed_by_module: &ExposedByModule, derived_module: SharedDerivedModule, ) -> Msg<'a> { let make_specializations_start = Instant::now(); let mut update_mode_ids = UpdateModeIds::new(); // do the thing let mut mono_env = roc_mono::ir::Env { arena, subs: &mut subs, home, ident_ids: &mut ident_ids, target_info, update_mode_ids: &mut update_mode_ids, // call_specialization_counter=0 is reserved call_specialization_counter: 1, abilities: AbilitiesView::World(&world_abilities), exposed_by_module, derived_module: &derived_module, }; let mut procs = Procs::new_in(arena); for (symbol, partial_proc) in procs_base.partial_procs.into_iter() { procs.partial_procs.insert(symbol, partial_proc); } procs.module_thunks = procs_base.module_thunks; procs.runtime_errors = procs_base.runtime_errors; procs.imported_module_thunks = procs_base.imported_module_thunks; // TODO: for now this final specialization pass is sequential, // with no parallelization at all. We should try to parallelize // this, but doing so will require a redesign of Procs. procs = roc_mono::ir::specialize_all( &mut mono_env, procs, specializations_we_must_make, procs_base.host_specializations, &mut layout_cache, ); let external_specializations_requested = procs.externals_we_need.clone(); let (procedures, restored_procs_base) = procs.get_specialized_procs_without_rc(&mut mono_env); // Turn `Bytes.Decode.IdentId(238)` into `Bytes.Decode.238`, we rely on this in mono tests mono_env.home.register_debug_idents(mono_env.ident_ids); let make_specializations_end = Instant::now(); module_timing .make_specializations .push(make_specializations_end.duration_since(make_specializations_start)); Msg::MadeSpecializations { module_id: home, ident_ids, layout_cache, procs_base: restored_procs_base, procedures, update_mode_ids, subs, external_specializations_requested, module_timing, } } #[allow(clippy::too_many_arguments)] fn build_pending_specializations<'a>( arena: &'a Bump, execution_mode: ExecutionMode, solved_subs: Solved, imported_module_thunks: &'a [Symbol], home: ModuleId, mut ident_ids: IdentIds, declarations: Declarations, mut module_timing: ModuleTiming, mut layout_cache: LayoutCache<'a>, target_info: TargetInfo, exposed_to_host: ExposedToHost, exposed_by_module: &ExposedByModule, abilities_store: AbilitiesStore, derived_module: SharedDerivedModule, ) -> Msg<'a> { let find_specializations_start = Instant::now(); let mut module_thunks = bumpalo::collections::Vec::new_in(arena); let mut toplevel_expects = ToplevelExpects::default(); let mut procs_base = ProcsBase { partial_procs: BumpMap::default(), module_thunks: &[], host_specializations: roc_mono::ir::HostSpecializations::new(), runtime_errors: BumpMap::default(), imported_module_thunks, }; let mut update_mode_ids = UpdateModeIds::new(); let mut subs = solved_subs.into_inner(); let mut mono_env = roc_mono::ir::Env { arena, subs: &mut subs, home, ident_ids: &mut ident_ids, target_info, update_mode_ids: &mut update_mode_ids, // call_specialization_counter=0 is reserved call_specialization_counter: 1, // NB: for getting pending specializations the module view is enough because we only need // to know the types and abilities in our modules. Only for building *all* specializations // do we need a global view. abilities: AbilitiesView::Module(&abilities_store), exposed_by_module, derived_module: &derived_module, }; // Add modules' decls to Procs for index in 0..declarations.len() { use roc_can::expr::DeclarationTag::*; let symbol = declarations.symbols[index].value; let expr_var = declarations.variables[index]; let is_host_exposed = exposed_to_host.values.contains_key(&symbol); // TODO remove clones (with drain) let annotation = declarations.annotations[index].clone(); let body = declarations.expressions[index].clone(); let tag = declarations.declarations[index]; match tag { Value => { // mark this symbols as a top-level thunk before any other work on the procs module_thunks.push(symbol); // If this is an exposed symbol, we need to // register it as such. Otherwise, since it // never gets called by Roc code, it will never // get specialized! if is_host_exposed { let layout_result = layout_cache.raw_from_var(mono_env.arena, expr_var, mono_env.subs); // cannot specialize when e.g. main's type contains type variables if let Err(e) = layout_result { match e { LayoutProblem::Erroneous => { let message = "top level function has erroneous type"; procs_base.runtime_errors.insert(symbol, message); continue; } LayoutProblem::UnresolvedTypeVar(v) => { let message = format!( "top level function has unresolved type variable {:?}", v ); procs_base .runtime_errors .insert(symbol, mono_env.arena.alloc(message)); continue; } } } procs_base.host_specializations.insert_host_exposed( mono_env.subs, LambdaName::no_niche(symbol), annotation, expr_var, ); } let proc = PartialProc { annotation: expr_var, // This is a 0-arity thunk, so it has no arguments. pattern_symbols: &[], // This is a top-level definition, so it cannot capture anything captured_symbols: CapturedSymbols::None, body: body.value, body_var: expr_var, // This is a 0-arity thunk, so it cannot be recursive is_self_recursive: false, }; procs_base.partial_procs.insert(symbol, proc); } Function(f_index) | Recursive(f_index) | TailRecursive(f_index) => { let function_def = &declarations.function_bodies[f_index.index()].value; // this is a top-level definition, it should not capture anything debug_assert!( function_def.captured_symbols.is_empty(), "{:?}", (symbol, symbol.module_id(), &function_def.captured_symbols) ); // If this is an exposed symbol, we need to // register it as such. Otherwise, since it // never gets called by Roc code, it will never // get specialized! if is_host_exposed { let layout_result = layout_cache.raw_from_var(mono_env.arena, expr_var, mono_env.subs); // cannot specialize when e.g. main's type contains type variables if let Err(e) = layout_result { match e { LayoutProblem::Erroneous => { let message = "top level function has erroneous type"; procs_base.runtime_errors.insert(symbol, message); continue; } LayoutProblem::UnresolvedTypeVar(v) => { let message = format!( "top level function has unresolved type variable {:?}", v ); procs_base .runtime_errors .insert(symbol, mono_env.arena.alloc(message)); continue; } } } procs_base.host_specializations.insert_host_exposed( mono_env.subs, LambdaName::no_niche(symbol), annotation, expr_var, ); } let is_recursive = matches!(tag, Recursive(_) | TailRecursive(_)); let partial_proc = PartialProc::from_named_function( &mut mono_env, expr_var, function_def.arguments.clone(), body, CapturedSymbols::None, is_recursive, function_def.return_type, ); procs_base.partial_procs.insert(symbol, partial_proc); } Destructure(d_index) => { let loc_pattern = &declarations.destructs[d_index.index()].loc_pattern; use roc_can::pattern::Pattern; let symbol = match &loc_pattern.value { Pattern::Identifier(_) => { debug_assert!(false, "identifier ended up in Destructure {:?}", symbol); symbol } Pattern::AbilityMemberSpecialization { ident, specializes } => { debug_assert!( false, "ability member ended up in Destructure {:?} specializes {:?}", ident, specializes ); symbol } Pattern::Shadowed(_, _, shadowed) => { // this seems to work for now *shadowed } _ => todo!("top-level destrucuture patterns are not implemented"), }; // mark this symbols as a top-level thunk before any other work on the procs module_thunks.push(symbol); // If this is an exposed symbol, we need to // register it as such. Otherwise, since it // never gets called by Roc code, it will never // get specialized! if is_host_exposed { let layout_result = layout_cache.raw_from_var(mono_env.arena, expr_var, mono_env.subs); // cannot specialize when e.g. main's type contains type variables if let Err(e) = layout_result { match e { LayoutProblem::Erroneous => { let message = "top level function has erroneous type"; procs_base.runtime_errors.insert(symbol, message); continue; } LayoutProblem::UnresolvedTypeVar(v) => { let message = format!( "top level function has unresolved type variable {:?}", v ); procs_base .runtime_errors .insert(symbol, mono_env.arena.alloc(message)); continue; } } } procs_base.host_specializations.insert_host_exposed( mono_env.subs, LambdaName::no_niche(symbol), annotation, expr_var, ); } let proc = PartialProc { annotation: expr_var, // This is a 0-arity thunk, so it has no arguments. pattern_symbols: &[], // This is a top-level definition, so it cannot capture anything captured_symbols: CapturedSymbols::None, body: body.value, body_var: expr_var, // This is a 0-arity thunk, so it cannot be recursive is_self_recursive: false, }; procs_base.partial_procs.insert(symbol, proc); } MutualRecursion { .. } => { // the declarations of this group will be treaded individually by later iterations } Expectation => { // skip expectations if we're not going to run them match execution_mode { ExecutionMode::Test => { /* fall through */ } ExecutionMode::Check | ExecutionMode::Executable | ExecutionMode::ExecutableIfCheck => continue, } // mark this symbol as a top-level thunk before any other work on the procs module_thunks.push(symbol); let expr_var = Variable::EMPTY_RECORD; let is_host_exposed = true; // If this is an exposed symbol, we need to // register it as such. Otherwise, since it // never gets called by Roc code, it will never // get specialized! if is_host_exposed { let layout_result = layout_cache.raw_from_var(mono_env.arena, expr_var, mono_env.subs); // cannot specialize when e.g. main's type contains type variables if let Err(e) = layout_result { match e { LayoutProblem::Erroneous => { let message = "top level function has erroneous type"; procs_base.runtime_errors.insert(symbol, message); continue; } LayoutProblem::UnresolvedTypeVar(v) => { let message = format!( "top level function has unresolved type variable {:?}", v ); procs_base .runtime_errors .insert(symbol, mono_env.arena.alloc(message)); continue; } } } procs_base.host_specializations.insert_host_exposed( mono_env.subs, LambdaName::no_niche(symbol), annotation, expr_var, ); } let body = roc_can::expr::toplevel_expect_to_inline_expect_pure(body); let proc = PartialProc { annotation: expr_var, // This is a 0-arity thunk, so it has no arguments. pattern_symbols: &[], // This is a top-level definition, so it cannot capture anything captured_symbols: CapturedSymbols::None, body: body.value, body_var: expr_var, // This is a 0-arity thunk, so it cannot be recursive is_self_recursive: false, }; // extend the region of the expect expression with the region of the preceding // comment, so it is shown in failure/panic messages let name_region = declarations.symbols[index].region; let expr_region = declarations.expressions[index].region; let region = Region::span_across(&name_region, &expr_region); toplevel_expects.pure.insert(symbol, region); procs_base.partial_procs.insert(symbol, proc); } ExpectationFx => { // skip expectations if we're not going to run them match execution_mode { ExecutionMode::Test => { /* fall through */ } ExecutionMode::Check | ExecutionMode::Executable | ExecutionMode::ExecutableIfCheck => continue, } // mark this symbol as a top-level thunk before any other work on the procs module_thunks.push(symbol); let expr_var = Variable::EMPTY_RECORD; let is_host_exposed = true; // If this is an exposed symbol, we need to // register it as such. Otherwise, since it // never gets called by Roc code, it will never // get specialized! if is_host_exposed { let layout_result = layout_cache.raw_from_var(mono_env.arena, expr_var, mono_env.subs); // cannot specialize when e.g. main's type contains type variables if let Err(e) = layout_result { match e { LayoutProblem::Erroneous => { let message = "top level function has erroneous type"; procs_base.runtime_errors.insert(symbol, message); continue; } LayoutProblem::UnresolvedTypeVar(v) => { let message = format!( "top level function has unresolved type variable {:?}", v ); procs_base .runtime_errors .insert(symbol, mono_env.arena.alloc(message)); continue; } } } procs_base.host_specializations.insert_host_exposed( mono_env.subs, LambdaName::no_niche(symbol), annotation, expr_var, ); } let body = roc_can::expr::toplevel_expect_to_inline_expect_fx(body); let proc = PartialProc { annotation: expr_var, // This is a 0-arity thunk, so it has no arguments. pattern_symbols: &[], // This is a top-level definition, so it cannot capture anything captured_symbols: CapturedSymbols::None, body: body.value, body_var: expr_var, // This is a 0-arity thunk, so it cannot be recursive is_self_recursive: false, }; // extend the region of the expect expression with the region of the preceding // comment, so it is shown in failure/panic messages let name_region = declarations.symbols[index].region; let expr_region = declarations.expressions[index].region; let region = Region::span_across(&name_region, &expr_region); toplevel_expects.fx.insert(symbol, region); procs_base.partial_procs.insert(symbol, proc); } } } procs_base.module_thunks = module_thunks.into_bump_slice(); let find_specializations_end = Instant::now(); module_timing.find_specializations = find_specializations_end.duration_since(find_specializations_start); Msg::FoundSpecializations { module_id: home, solved_subs: Solved(subs), ident_ids, layout_cache, procs_base, module_timing, abilities_store, toplevel_expects, } } /// Loads derived ability members up for specialization into the Derived module, prior to making /// their specializations. // TODO: right now, this runs sequentially, and no other modules are mono'd in parallel to the // derived module. #[allow(clippy::too_many_arguments)] fn load_derived_partial_procs<'a>( home: ModuleId, arena: &'a Bump, subs: &mut Subs, ident_ids: &mut IdentIds, derived_module: &SharedDerivedModule, module_timing: &mut ModuleTiming, target_info: TargetInfo, exposed_by_module: &ExposedByModule, procs_base: &mut ProcsBase<'a>, world_abilities: &mut WorldAbilities, ) { debug_assert_eq!(home, ModuleId::DERIVED_GEN); let load_derived_procs_start = Instant::now(); let mut new_module_thunks = bumpalo::collections::Vec::new_in(arena); let mut update_mode_ids = UpdateModeIds::new(); let derives_to_add = { let mut derived_module = derived_module.lock().unwrap(); derived_module.iter_load_for_gen_module(subs, |symbol| { !procs_base.partial_procs.contains_key(&symbol) }) }; // TODO: we can be even lazier here if we move `add_def_to_module` to happen in mono. Also, the // timings would be more accurate. for (derived_symbol, (derived_expr, derived_expr_var)) in derives_to_add.into_iter() { let mut mono_env = roc_mono::ir::Env { arena, subs, home, ident_ids, target_info, update_mode_ids: &mut update_mode_ids, // call_specialization_counter=0 is reserved call_specialization_counter: 1, // NB: for getting pending specializations the module view is enough because we only need // to know the types and abilities in our modules. Only for building *all* specializations // do we need a global view. abilities: AbilitiesView::World(world_abilities), exposed_by_module, derived_module, }; let partial_proc = match derived_expr { roc_can::expr::Expr::Closure(roc_can::expr::ClosureData { function_type, arguments, loc_body, captured_symbols, return_type, recursive, .. }) => { debug_assert!(captured_symbols.is_empty()); PartialProc::from_named_function( &mut mono_env, function_type, arguments.clone(), *loc_body, CapturedSymbols::None, recursive.is_recursive(), return_type, ) } _ => { // mark this symbols as a top-level thunk before any other work on the procs new_module_thunks.push(derived_symbol); PartialProc { annotation: derived_expr_var, // This is a 0-arity thunk, so it has no arguments. pattern_symbols: &[], // This is a top-level definition, so it cannot capture anything captured_symbols: CapturedSymbols::None, body: derived_expr, body_var: derived_expr_var, // This is a 0-arity thunk, so it cannot be recursive is_self_recursive: false, } } }; procs_base .partial_procs .insert(derived_symbol, partial_proc); } if !new_module_thunks.is_empty() { new_module_thunks.extend(procs_base.module_thunks); procs_base.module_thunks = new_module_thunks.into_bump_slice(); } let load_derived_procs_end = Instant::now(); module_timing.find_specializations = load_derived_procs_end.duration_since(load_derived_procs_start); } fn run_task<'a>( task: BuildTask<'a>, arena: &'a Bump, src_dir: &Path, msg_tx: MsgSender<'a>, target_info: TargetInfo, ) -> Result<(), LoadingProblem<'a>> { use BuildTask::*; let msg = match task { LoadModule { module_name, module_ids, shorthands, ident_ids_by_module, } => load_module( arena, src_dir, module_name, module_ids, shorthands, ident_ids_by_module, ) .map(|(_, msg)| msg), Parse { header } => parse(arena, header), CanonicalizeAndConstrain { parsed, module_ids, dep_idents, exposed_symbols, aliases, abilities_store, skip_constraint_gen, } => { let can_and_con = canonicalize_and_constrain( arena, &module_ids, dep_idents, exposed_symbols, aliases, abilities_store, parsed, skip_constraint_gen, ); Ok(Msg::CanonicalizedAndConstrained(can_and_con)) } Solve { module, module_timing, exposed_for_module, constraints, constraint, pending_derives, var_store, ident_ids, declarations, dep_idents, cached_subs, derived_module, } => Ok(run_solve( module, ident_ids, module_timing, exposed_for_module, constraints, constraint, pending_derives, var_store, declarations, dep_idents, cached_subs, derived_module, )), BuildPendingSpecializations { module_id, execution_mode, ident_ids, decls, module_timing, layout_cache, solved_subs, imported_module_thunks, exposed_to_host, abilities_store, exposed_by_module, derived_module, } => Ok(build_pending_specializations( arena, execution_mode, solved_subs, imported_module_thunks, module_id, ident_ids, decls, module_timing, layout_cache, target_info, exposed_to_host, &exposed_by_module, abilities_store, derived_module, )), MakeSpecializations { module_id, ident_ids, subs, procs_base, layout_cache, specializations_we_must_make, module_timing, world_abilities, exposed_by_module, derived_module, } => Ok(make_specializations( arena, module_id, ident_ids, subs, procs_base, layout_cache, specializations_we_must_make, module_timing, target_info, world_abilities, &exposed_by_module, derived_module, )), }?; msg_tx .send(msg) .map_err(|_| LoadingProblem::MsgChannelDied)?; Ok(()) } fn to_file_problem_report(filename: &Path, error: io::ErrorKind) -> String { use roc_reporting::report::{Report, RocDocAllocator, Severity, DEFAULT_PALETTE}; use ven_pretty::DocAllocator; let src_lines: Vec<&str> = Vec::new(); let mut module_ids = ModuleIds::default(); let module_id = module_ids.get_or_insert(&"find module name somehow?".into()); let interns = Interns::default(); // Report parsing and canonicalization problems let alloc = RocDocAllocator::new(&src_lines, module_id, &interns); let report = match error { io::ErrorKind::NotFound => { let doc = alloc.stack([ alloc.reflow(r"I am looking for this file, but it's not there:"), alloc .parser_suggestion(filename.to_str().unwrap()) .indent(4), alloc.concat([ alloc.reflow(r"Is the file supposed to be there? "), alloc.reflow("Maybe there is a typo in the file name?"), ]), ]); Report { filename: "UNKNOWN.roc".into(), doc, title: "FILE NOT FOUND".to_string(), severity: Severity::RuntimeError, } } io::ErrorKind::PermissionDenied => { let doc = alloc.stack([ alloc.reflow(r"I don't have the required permissions to read this file:"), alloc .parser_suggestion(filename.to_str().unwrap()) .indent(4), alloc .concat([alloc.reflow(r"Is it the right file? Maybe change its permissions?")]), ]); Report { filename: "UNKNOWN.roc".into(), doc, title: "FILE PERMISSION DENIED".to_string(), severity: Severity::RuntimeError, } } _ => { let error = std::io::Error::from(error); let formatted = format!("{}", error); let doc = alloc.concat([ alloc.reflow(r"I tried to read this file, but ran into a "), alloc.text(formatted), alloc.reflow(r" problem."), ]); Report { filename: "UNKNOWN.roc".into(), doc, title: "FILE PROBLEM".to_string(), severity: Severity::RuntimeError, } } }; let mut buf = String::new(); let palette = DEFAULT_PALETTE; report.render_color_terminal(&mut buf, &alloc, &palette); buf } fn to_parse_problem_report<'a>( problem: FileError<'a, SyntaxError<'a>>, mut module_ids: ModuleIds, all_ident_ids: IdentIdsByModule, render: RenderTarget, ) -> String { use roc_reporting::report::{parse_problem, RocDocAllocator, DEFAULT_PALETTE}; // TODO this is not in fact safe let src = unsafe { from_utf8_unchecked(problem.problem.bytes) }; let src_lines = src.lines().collect::>(); // let mut src_lines: Vec<&str> = problem.prefix.lines().collect(); // src_lines.extend(src.lines().skip(1)); let module_id = module_ids.get_or_insert(&"find module name somehow?".into()); let interns = Interns { module_ids, all_ident_ids, }; // Report parsing and canonicalization problems let alloc = RocDocAllocator::new(&src_lines, module_id, &interns); let starting_line = 0; let lines = LineInfo::new(src); let report = parse_problem( &alloc, &lines, problem.filename.clone(), starting_line, problem, ); let mut buf = String::new(); let palette = DEFAULT_PALETTE; report.render(render, &mut buf, &alloc, &palette); buf } fn to_missing_platform_report(module_id: ModuleId, other: PlatformPath) -> String { use roc_reporting::report::{Report, RocDocAllocator, Severity, DEFAULT_PALETTE}; use ven_pretty::DocAllocator; use PlatformPath::*; // Report parsing and canonicalization problems let interns = Interns::default(); let alloc = RocDocAllocator::new(&[], module_id, &interns); let report = { match other { Valid(_) => unreachable!(), NotSpecified => { let doc = alloc.stack([ alloc.reflow("I could not find a platform based on your input file."), alloc.reflow(r"Does the module header contain an entry that looks like this:"), alloc .parser_suggestion(" packages { pf: \"platform\" }") .indent(4), alloc.reflow("See also TODO."), ]); Report { filename: "UNKNOWN.roc".into(), doc, title: "NO PLATFORM".to_string(), severity: Severity::RuntimeError, } } RootIsInterface => { let doc = alloc.stack([ alloc.reflow( r"The input file is an `interface` module, but only `app` modules can be run.", ), alloc.reflow(r"Tip: You can use `roc check` or `roc test` to verify an interface module like this one."), ]); Report { filename: "UNKNOWN.roc".into(), doc, title: "NO PLATFORM".to_string(), severity: Severity::RuntimeError, } } RootIsHosted => { let doc = alloc.stack([ alloc.reflow( r"The input file is a `hosted` module, but only `app` modules can be run.", ), alloc.reflow(r"Tip: You can use `roc check` or `roc test` to verify a hosted module like this one."), ]); Report { filename: "UNKNOWN.roc".into(), doc, title: "NO PLATFORM".to_string(), severity: Severity::RuntimeError, } } RootIsPlatformModule => { let doc = alloc.stack([ alloc.reflow( r"The input file is a `platform` module, but only `app` modules can be run.", ), alloc.reflow(r"Tip: You can use `roc check` or `roc test` to verify a platform module like this one."), ]); Report { filename: "UNKNOWN.roc".into(), doc, title: "NO PLATFORM".to_string(), severity: Severity::RuntimeError, } } } }; let palette = DEFAULT_PALETTE; let mut buf = String::new(); report.render_color_terminal(&mut buf, &alloc, &palette); buf } /// Builtin aliases that are not covered by type checker optimizations /// /// Types like `F64` and `I32` are hardcoded into Subs and therefore we don't define them here. /// Generic number types (Num, Int, Float, etc.) are treated as `DelayedAlias`es resolved during /// type solving. /// All that remains are Signed8, Signed16, etc. pub fn default_aliases() -> roc_solve::solve::Aliases { use roc_types::types::Type; let mut solve_aliases = roc_solve::solve::Aliases::default(); let mut zero_opaque = |alias_name: Symbol| { let alias = Alias { region: Region::zero(), type_variables: vec![], lambda_set_variables: Default::default(), recursion_variables: Default::default(), typ: Type::EmptyTagUnion, kind: AliasKind::Opaque, }; solve_aliases.insert(alias_name, alias); }; zero_opaque(Symbol::NUM_SIGNED8); zero_opaque(Symbol::NUM_SIGNED16); zero_opaque(Symbol::NUM_SIGNED32); zero_opaque(Symbol::NUM_SIGNED64); zero_opaque(Symbol::NUM_SIGNED128); zero_opaque(Symbol::NUM_UNSIGNED8); zero_opaque(Symbol::NUM_UNSIGNED16); zero_opaque(Symbol::NUM_UNSIGNED32); zero_opaque(Symbol::NUM_UNSIGNED64); zero_opaque(Symbol::NUM_UNSIGNED128); zero_opaque(Symbol::NUM_BINARY32); zero_opaque(Symbol::NUM_BINARY64); solve_aliases }