Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / compiler-core / src / build / package_compiler.rs
23 kB 710 lines
1use crate::analyse::{ModuleAnalyzerConstructor, TargetSupport}; 2use crate::line_numbers::{self, LineNumbers}; 3use crate::type_::PRELUDE_MODULE_NAME; 4use crate::{ 5 Error, Result, Warning, 6 ast::{SrcSpan, TypedModule, UntypedModule}, 7 build::{ 8 Mode, Module, Origin, Outcome, Package, SourceFingerprint, Target, 9 elixir_libraries::ElixirLibraries, 10 native_file_copier::NativeFileCopier, 11 package_loader::{CodegenRequired, PackageLoader, StaleTracker}, 12 }, 13 codegen::{Erlang, ErlangApp, JavaScript, TypeScriptDeclarations}, 14 config::PackageConfig, 15 dep_tree, error, 16 io::{BeamCompiler, CommandExecutor, FileSystemReader, FileSystemWriter, Stdio}, 17 metadata::ModuleEncoder, 18 parse::extra::ModuleExtra, 19 paths, type_, 20 uid::UniqueIdGenerator, 21 warning::{TypeWarningEmitter, WarningEmitter}, 22}; 23use askama::Template; 24use ecow::EcoString; 25use std::collections::HashSet; 26use std::{collections::HashMap, fmt::write, time::SystemTime}; 27use vec1::Vec1; 28 29use camino::{Utf8Path, Utf8PathBuf}; 30 31use super::{ErlangAppCodegenConfiguration, TargetCodegenConfiguration, Telemetry}; 32 33pub struct Compiled { 34 pub modules: Vec<Module>, 35 pub module_names: Vec<EcoString>, 36} 37 38#[derive(Debug)] 39pub struct PackageCompiler<'a, IO> { 40 pub io: IO, 41 pub out: &'a Utf8Path, 42 pub lib: &'a Utf8Path, 43 pub root: &'a Utf8Path, 44 pub mode: Mode, 45 pub target: &'a TargetCodegenConfiguration, 46 pub config: &'a PackageConfig, 47 pub ids: UniqueIdGenerator, 48 pub write_metadata: bool, 49 pub perform_codegen: bool, 50 /// If set to false the compiler won't load and analyse any of the package's 51 /// modules and always succeed compilation returning no compile modules. 52 /// 53 /// Code generation is still carried out so that a root package will have an 54 /// entry point nonetheless. 55 /// 56 pub compile_modules: bool, 57 pub write_entrypoint: bool, 58 pub copy_native_files: bool, 59 pub compile_beam_bytecode: bool, 60 pub subprocess_stdio: Stdio, 61 pub target_support: TargetSupport, 62 pub cached_warnings: CachedWarnings, 63} 64 65impl<'a, IO> PackageCompiler<'a, IO> 66where 67 IO: FileSystemReader + FileSystemWriter + CommandExecutor + BeamCompiler + Clone, 68{ 69 pub fn new( 70 config: &'a PackageConfig, 71 mode: Mode, 72 root: &'a Utf8Path, 73 out: &'a Utf8Path, 74 lib: &'a Utf8Path, 75 target: &'a TargetCodegenConfiguration, 76 ids: UniqueIdGenerator, 77 io: IO, 78 ) -> Self { 79 Self { 80 io, 81 ids, 82 out, 83 lib, 84 root, 85 mode, 86 config, 87 target, 88 write_metadata: true, 89 perform_codegen: true, 90 compile_modules: true, 91 write_entrypoint: false, 92 copy_native_files: true, 93 compile_beam_bytecode: true, 94 subprocess_stdio: Stdio::Inherit, 95 target_support: TargetSupport::NotEnforced, 96 cached_warnings: CachedWarnings::Ignore, 97 } 98 } 99 100 /// Compile the package. 101 /// Returns a list of modules that were compiled. Any modules that were read 102 /// from the cache will not be returned. 103 // TODO: return the cached modules. 104 pub fn compile( 105 mut self, 106 warnings: &WarningEmitter, 107 existing_modules: &mut im::HashMap<EcoString, type_::ModuleInterface>, 108 already_defined_modules: &mut im::HashMap<EcoString, Utf8PathBuf>, 109 stale_modules: &mut StaleTracker, 110 incomplete_modules: &mut HashSet<EcoString>, 111 telemetry: &dyn Telemetry, 112 ) -> Outcome<Compiled, Error> { 113 let span = tracing::info_span!("compile", package = %self.config.name.as_str()); 114 let _enter = span.enter(); 115 116 // Ensure that the package is compatible with this version of Gleam 117 if let Err(e) = self.config.check_gleam_compatibility() { 118 return e.into(); 119 } 120 121 let artefact_directory = self.out.join(paths::ARTEFACT_DIRECTORY_NAME); 122 let codegen_required = if self.perform_codegen { 123 CodegenRequired::Yes 124 } else { 125 CodegenRequired::No 126 }; 127 128 let loader = PackageLoader::new( 129 self.io.clone(), 130 self.ids.clone(), 131 self.mode, 132 self.root, 133 self.cached_warnings, 134 warnings, 135 codegen_required, 136 &artefact_directory, 137 self.target.target(), 138 &self.config.name, 139 stale_modules, 140 already_defined_modules, 141 incomplete_modules, 142 ); 143 144 let loaded = if self.compile_modules { 145 match loader.run() { 146 Ok(loaded) => loaded, 147 Err(error) => return error.into(), 148 } 149 } else { 150 Loaded::empty() 151 }; 152 153 let mut module_names = Vec::new(); 154 155 // Load the cached modules that have previously been compiled 156 for module in loaded.cached.into_iter() { 157 // Emit any cached warnings. 158 // Note that `self.cached_warnings` is set to `Ignore` (such as for 159 // dependency packages) then this field will not be populated. 160 if let Err(e) = self.emit_warnings(warnings, &module) { 161 return e.into(); 162 } 163 164 module_names.push(module.name.clone()); 165 166 // Register the cached module so its type information etc can be 167 // used for compiling futher modules. 168 _ = existing_modules.insert(module.name.clone(), module); 169 } 170 171 if !loaded.to_compile.is_empty() { 172 // Print that work is being done 173 if self.perform_codegen { 174 telemetry.compiling_package(&self.config.name); 175 } else { 176 telemetry.checking_package(&self.config.name) 177 } 178 } 179 180 // Type check the modules that are new or have changed 181 tracing::info!(count=%loaded.to_compile.len(), "analysing_modules"); 182 let outcome = analyse( 183 &self.config, 184 self.target.target(), 185 self.mode, 186 &self.ids, 187 loaded.to_compile, 188 existing_modules, 189 warnings, 190 self.target_support, 191 incomplete_modules, 192 ); 193 194 let mut modules = match outcome { 195 Outcome::Ok(modules) => modules, 196 Outcome::PartialFailure(modules, error) => { 197 return Outcome::PartialFailure( 198 Compiled { 199 modules, 200 module_names, 201 }, 202 error, 203 ) 204 } 205 Outcome::TotalFailure(error) => return Outcome::TotalFailure(error), 206 }; 207 208 for mut module in modules.iter_mut() { 209 module.attach_doc_and_module_comments(); 210 } 211 212 tracing::debug!("performing_code_generation"); 213 214 if let Err(error) = self.perform_codegen(&modules) { 215 return error.into(); 216 } 217 218 if let Err(error) = self.encode_and_write_metadata(&modules) { 219 return error.into(); 220 } 221 222 Outcome::Ok(Compiled { 223 modules, 224 module_names, 225 }) 226 } 227 228 fn compile_erlang_to_beam( 229 &mut self, 230 modules: &HashSet<Utf8PathBuf>, 231 ) -> Result<Vec<EcoString>, Error> { 232 if modules.is_empty() { 233 tracing::debug!("no_erlang_to_compile"); 234 return Ok(Vec::new()); 235 } 236 237 tracing::debug!("compiling_erlang"); 238 239 self.io 240 .compile_beam(self.out, self.lib, modules, self.subprocess_stdio) 241 .map(|modules| modules.iter().map(|str| EcoString::from(str)).collect()) 242 } 243 244 fn copy_project_native_files( 245 &mut self, 246 destination_dir: &Utf8Path, 247 to_compile_modules: &mut HashSet<Utf8PathBuf>, 248 ) -> Result<(), Error> { 249 tracing::debug!("copying_native_source_files"); 250 251 // TODO: unit test 252 let priv_source = self.root.join("priv"); 253 let priv_build = self.out.join("priv"); 254 if self.io.is_directory(&priv_source) && !self.io.is_directory(&priv_build) { 255 tracing::debug!("linking_priv_to_build"); 256 self.io.symlink_dir(&priv_source, &priv_build)?; 257 } 258 259 let copier = NativeFileCopier::new(self.io.clone(), self.root.clone(), destination_dir); 260 let copied = copier.run()?; 261 262 to_compile_modules.extend(copied.to_compile.into_iter()); 263 264 // If there are any Elixir files then we need to locate Elixir 265 // installed on this system for use in compilation. 266 if copied.any_elixir { 267 ElixirLibraries::make_available( 268 &self.io, 269 &self.lib.to_path_buf(), 270 self.subprocess_stdio, 271 )?; 272 } 273 274 Ok(()) 275 } 276 277 fn encode_and_write_metadata(&mut self, modules: &[Module]) -> Result<()> { 278 if !self.write_metadata { 279 tracing::debug!("package_metadata_writing_disabled"); 280 return Ok(()); 281 } 282 if modules.is_empty() { 283 return Ok(()); 284 } 285 286 let artefact_dir = self.out.join(paths::ARTEFACT_DIRECTORY_NAME); 287 288 tracing::debug!("writing_module_caches"); 289 for module in modules { 290 let module_name = module.name.replace("/", "@"); 291 292 // Write metadata file 293 let name = format!("{}.cache", &module_name); 294 let path = artefact_dir.join(name); 295 let bytes = ModuleEncoder::new(&module.ast.type_info).encode()?; 296 self.io.write_bytes(&path, &bytes)?; 297 298 // Write cache info 299 let name = format!("{}.cache_meta", &module_name); 300 let path = artefact_dir.join(name); 301 let info = CacheMetadata { 302 mtime: module.mtime, 303 codegen_performed: self.perform_codegen, 304 dependencies: module.dependencies.clone(), 305 fingerprint: SourceFingerprint::new(&module.code), 306 line_numbers: module.ast.type_info.line_numbers.clone(), 307 }; 308 self.io.write_bytes(&path, &info.to_binary())?; 309 310 // Write warnings. 311 // Dependency packages don't get warnings persisted as the 312 // programmer doesn't want to be told every time about warnings they 313 // cannot fix directly. 314 if self.cached_warnings.should_use() { 315 let name = format!("{}.cache_warnings", &module_name); 316 let path = artefact_dir.join(name); 317 let warnings = &module.ast.type_info.warnings; 318 let data = bincode::serialize(warnings).expect("Serialise warnings"); 319 self.io.write_bytes(&path, &data)?; 320 } 321 } 322 Ok(()) 323 } 324 325 fn perform_codegen(&mut self, modules: &[Module]) -> Result<()> { 326 if !self.perform_codegen { 327 tracing::debug!("skipping_codegen"); 328 return Ok(()); 329 } 330 331 match self.target { 332 TargetCodegenConfiguration::JavaScript { 333 emit_typescript_definitions, 334 prelude_location, 335 } => self.perform_javascript_codegen( 336 modules, 337 *emit_typescript_definitions, 338 prelude_location, 339 ), 340 TargetCodegenConfiguration::Erlang { app_file } => { 341 self.perform_erlang_codegen(modules, app_file.as_ref()) 342 } 343 } 344 } 345 346 fn perform_erlang_codegen( 347 &mut self, 348 modules: &[Module], 349 app_file_config: Option<&ErlangAppCodegenConfiguration>, 350 ) -> Result<(), Error> { 351 let mut written = HashSet::new(); 352 let build_dir = self.out.join(paths::ARTEFACT_DIRECTORY_NAME); 353 let include_dir = self.out.join("include"); 354 let io = self.io.clone(); 355 356 io.mkdir(&build_dir)?; 357 358 if self.copy_native_files { 359 self.copy_project_native_files(&build_dir, &mut written)?; 360 } else { 361 tracing::debug!("skipping_native_file_copying"); 362 } 363 364 if self.compile_beam_bytecode && self.write_entrypoint { 365 self.render_erlang_entrypoint_module(&build_dir, &mut written)?; 366 } else { 367 tracing::debug!("skipping_entrypoint_generation"); 368 } 369 370 // NOTE: This must come after `copy_project_native_files` to ensure that 371 // we overwrite any precompiled Erlang that was included in the Hex 372 // package. Otherwise we will build the potentially outdated precompiled 373 // version and not the newly compiled version. 374 Erlang::new(&build_dir, &include_dir).render(io.clone(), modules, self.root)?; 375 376 let native_modules: Vec<EcoString> = if self.compile_beam_bytecode { 377 written.extend(modules.iter().map(Module::compiled_erlang_path)); 378 self.compile_erlang_to_beam(&written)? 379 } else { 380 tracing::debug!("skipping_erlang_bytecode_compilation"); 381 Vec::new() 382 }; 383 384 if let Some(config) = app_file_config { 385 ErlangApp::new(&self.out.join("ebin"), config).render( 386 io, 387 &self.config, 388 modules, 389 native_modules, 390 )?; 391 } 392 Ok(()) 393 } 394 395 fn perform_javascript_codegen( 396 &mut self, 397 modules: &[Module], 398 typescript: bool, 399 prelude_location: &Utf8Path, 400 ) -> Result<(), Error> { 401 let mut written = HashSet::new(); 402 let typescript = if typescript { 403 TypeScriptDeclarations::Emit 404 } else { 405 TypeScriptDeclarations::None 406 }; 407 408 JavaScript::new(&self.out, typescript, prelude_location, self.target_support) 409 .render(&self.io, modules)?; 410 411 if self.copy_native_files { 412 self.copy_project_native_files(&self.out, &mut written)?; 413 } else { 414 tracing::debug!("skipping_native_file_copying"); 415 } 416 417 Ok(()) 418 } 419 420 fn render_erlang_entrypoint_module( 421 &mut self, 422 out: &Utf8Path, 423 modules_to_compile: &mut HashSet<Utf8PathBuf>, 424 ) -> Result<(), Error> { 425 let name = format!("{name}@@main.erl", name = self.config.name); 426 let path = out.join(&name); 427 428 // If the entrypoint module has already been created then we don't need 429 // to write and compile it again. 430 if self.io.is_file(&path) { 431 tracing::debug!("erlang_entrypoint_already_exists"); 432 return Ok(()); 433 } 434 435 let template = ErlangEntrypointModule { 436 application: &self.config.name, 437 }; 438 let module = template.render().expect("Erlang entrypoint rendering"); 439 self.io.write(&path, &module)?; 440 let _ = modules_to_compile.insert(name.into()); 441 tracing::debug!("erlang_entrypoint_written"); 442 Ok(()) 443 } 444 445 fn emit_warnings( 446 &self, 447 warnings: &WarningEmitter, 448 module: &type_::ModuleInterface, 449 ) -> Result<()> { 450 for warning in &module.warnings { 451 let src = self.io.read(&module.src_path)?; 452 warnings.emit(Warning::Type { 453 path: module.src_path.clone(), 454 src: src.into(), 455 warning: warning.clone(), 456 }); 457 } 458 459 Ok(()) 460 } 461} 462 463fn analyse( 464 package_config: &PackageConfig, 465 target: Target, 466 mode: Mode, 467 ids: &UniqueIdGenerator, 468 mut parsed_modules: Vec<UncompiledModule>, 469 module_types: &mut im::HashMap<EcoString, type_::ModuleInterface>, 470 warnings: &WarningEmitter, 471 target_support: TargetSupport, 472 incomplete_modules: &mut HashSet<EcoString>, 473) -> Outcome<Vec<Module>, Error> { 474 let mut modules = Vec::with_capacity(parsed_modules.len() + 1); 475 let direct_dependencies = package_config.dependencies_for(mode).expect("Package deps"); 476 477 // Insert the prelude 478 // DUPE: preludeinsertion 479 // TODO: Currently we do this here and also in the tests. It would be better 480 // to have one place where we create all this required state for use in each 481 // place. 482 let _ = module_types.insert(PRELUDE_MODULE_NAME.into(), type_::build_prelude(ids)); 483 484 for UncompiledModule { 485 name, 486 code, 487 ast, 488 path, 489 mtime, 490 origin, 491 package, 492 dependencies, 493 extra, 494 } in parsed_modules 495 { 496 tracing::debug!(module = ?name, "Type checking"); 497 498 let line_numbers = LineNumbers::new(&code); 499 500 let analysis = crate::analyse::ModuleAnalyzerConstructor { 501 target, 502 ids, 503 origin, 504 importable_modules: module_types, 505 warnings: &TypeWarningEmitter::new(path.clone(), code.clone(), warnings.clone()), 506 direct_dependencies: &direct_dependencies, 507 target_support, 508 package_config, 509 } 510 .infer_module(ast, line_numbers, path.clone()); 511 512 match analysis { 513 Outcome::Ok(ast) => { 514 // Module has compiled successfully. Make sure it isn't marked as incomplete. 515 let _ = incomplete_modules.remove(&name.clone()); 516 // Register the types from this module so they can be imported into 517 // other modules. 518 let _ = module_types.insert(name.clone(), ast.type_info.clone()); 519 // Register the successfully type checked module data so that it can be 520 // used for code generation and in the language server. 521 modules.push(Module { 522 dependencies, 523 origin, 524 extra, 525 mtime, 526 name, 527 code, 528 ast, 529 input_path: path, 530 }); 531 } 532 533 Outcome::PartialFailure(ast, errors) => { 534 let error = Error::Type { 535 names: ast.names.clone(), 536 path: path.clone(), 537 src: code.clone(), 538 errors, 539 }; 540 // Mark as incomplete so that this module isn't reloaded from cache. 541 let _ = incomplete_modules.insert(name.clone()); 542 // Register the partially type checked module data so that it can be 543 // used in the language server. 544 modules.push(Module { 545 dependencies, 546 origin, 547 extra, 548 mtime, 549 name, 550 code, 551 ast, 552 input_path: path, 553 }); 554 // WARNING: This cannot be used for code generation as the code has errors. 555 return Outcome::PartialFailure(modules, error); 556 } 557 558 Outcome::TotalFailure(errors) => { 559 return Outcome::TotalFailure(Error::Type { 560 names: Default::default(), 561 path: path.clone(), 562 src: code.clone(), 563 errors, 564 }); 565 } 566 }; 567 } 568 569 Outcome::Ok(modules) 570} 571 572pub(crate) fn module_name(package_path: &Utf8Path, full_module_path: &Utf8Path) -> EcoString { 573 // /path/to/project/_build/default/lib/the_package/src/my/module.gleam 574 575 // my/module.gleam 576 let mut module_path = full_module_path 577 .strip_prefix(package_path) 578 .expect("Stripping package prefix from module path") 579 .to_path_buf(); 580 581 // my/module 582 let _ = module_path.set_extension(""); 583 584 // Stringify 585 let name = module_path.to_string(); 586 587 // normalise windows paths 588 name.replace("\\", "/").into() 589} 590 591#[derive(Debug)] 592pub(crate) enum Input { 593 New(UncompiledModule), 594 Cached(CachedModule), 595} 596 597impl Input { 598 pub fn name(&self) -> &EcoString { 599 match self { 600 Input::New(m) => &m.name, 601 Input::Cached(m) => &m.name, 602 } 603 } 604 605 pub fn source_path(&self) -> &Utf8Path { 606 match self { 607 Input::New(m) => &m.path, 608 Input::Cached(m) => &m.source_path, 609 } 610 } 611 612 pub fn dependencies(&self) -> Vec<EcoString> { 613 match self { 614 Input::New(m) => m.dependencies.iter().map(|(n, _)| n.clone()).collect(), 615 Input::Cached(m) => m.dependencies.iter().map(|(n, _)| n.clone()).collect(), 616 } 617 } 618 619 /// Returns `true` if the input is [`New`]. 620 /// 621 /// [`New`]: Input::New 622 #[must_use] 623 pub(crate) fn is_new(&self) -> bool { 624 matches!(self, Self::New(..)) 625 } 626 627 /// Returns `true` if the input is [`Cached`]. 628 /// 629 /// [`Cached`]: Input::Cached 630 #[must_use] 631 pub(crate) fn is_cached(&self) -> bool { 632 matches!(self, Self::Cached(..)) 633 } 634} 635 636#[derive(Debug)] 637pub(crate) struct CachedModule { 638 pub name: EcoString, 639 pub origin: Origin, 640 pub dependencies: Vec<(EcoString, SrcSpan)>, 641 pub source_path: Utf8PathBuf, 642 pub line_numbers: LineNumbers, 643} 644 645#[derive(Debug, serde::Serialize, serde::Deserialize)] 646pub(crate) struct CacheMetadata { 647 pub mtime: SystemTime, 648 pub codegen_performed: bool, 649 pub dependencies: Vec<(EcoString, SrcSpan)>, 650 pub fingerprint: SourceFingerprint, 651 pub line_numbers: LineNumbers, 652} 653 654impl CacheMetadata { 655 pub fn to_binary(&self) -> Vec<u8> { 656 bincode::serialize(self).expect("Serializing cache info") 657 } 658 659 pub fn from_binary(bytes: &[u8]) -> Result<Self, String> { 660 bincode::deserialize(bytes).map_err(|e| e.to_string()) 661 } 662} 663 664#[derive(Debug, Default, PartialEq, Eq)] 665pub(crate) struct Loaded { 666 pub to_compile: Vec<UncompiledModule>, 667 pub cached: Vec<type_::ModuleInterface>, 668} 669 670impl Loaded { 671 fn empty() -> Self { 672 Self { 673 to_compile: vec![], 674 cached: vec![], 675 } 676 } 677} 678 679#[derive(Debug, PartialEq, Eq)] 680pub(crate) struct UncompiledModule { 681 pub path: Utf8PathBuf, 682 pub name: EcoString, 683 pub code: EcoString, 684 pub mtime: SystemTime, 685 pub origin: Origin, 686 pub package: EcoString, 687 pub dependencies: Vec<(EcoString, SrcSpan)>, 688 pub ast: UntypedModule, 689 pub extra: ModuleExtra, 690} 691 692#[derive(Template)] 693#[template(path = "gleam@@main.erl", escape = "none")] 694struct ErlangEntrypointModule<'a> { 695 application: &'a str, 696} 697 698#[derive(Debug, Clone, Copy)] 699pub enum CachedWarnings { 700 Use, 701 Ignore, 702} 703impl CachedWarnings { 704 pub(crate) fn should_use(&self) -> bool { 705 match self { 706 CachedWarnings::Use => true, 707 CachedWarnings::Ignore => false, 708 } 709 } 710}