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
14 kB 471 lines
1use crate::{ 2 ast::{SrcSpan, TypedModule, UntypedModule}, 3 build::{dep_tree, Module, Origin, Package, Target}, 4 codegen::{Erlang, ErlangApp, JavaScript}, 5 config::PackageConfig, 6 error, 7 io::{CommandExecutor, FileSystemIO, FileSystemReader, FileSystemWriter}, 8 metadata::ModuleEncoder, 9 parse::extra::ModuleExtra, 10 type_, Error, Result, Warning, 11}; 12use askama::Template; 13use std::{collections::HashMap, fmt::write}; 14use std::{ 15 collections::HashSet, 16 path::{Path, PathBuf}, 17}; 18 19#[derive(Debug)] 20pub struct PackageCompiler<'a, IO> { 21 pub io: IO, 22 pub out: &'a Path, 23 pub root: &'a Path, 24 pub target: Target, 25 pub config: &'a PackageConfig, 26 pub sources: Vec<Source>, 27 pub erl_libs: &'a str, 28 pub write_metadata: bool, 29 pub compile_erlang: bool, 30 pub write_entrypoint: bool, 31} 32 33// TODO: ensure this is not a duplicate module 34// TODO: tests 35// Including cases for: 36// - modules that don't import anything 37impl<'a, IO> PackageCompiler<'a, IO> 38where 39 IO: FileSystemIO + CommandExecutor + Clone, 40{ 41 pub fn new( 42 config: &'a PackageConfig, 43 root: &'a Path, 44 out: &'a Path, 45 target: Target, 46 erl_libs: &'a str, 47 io: IO, 48 ) -> Self { 49 Self { 50 io, 51 out, 52 root, 53 config, 54 target, 55 sources: vec![], 56 erl_libs, 57 compile_erlang: true, 58 write_metadata: true, 59 write_entrypoint: false, 60 } 61 } 62 63 pub fn compile( 64 mut self, 65 warnings: &mut Vec<Warning>, 66 existing_modules: &mut HashMap<String, type_::Module>, 67 already_defined_modules: &mut HashMap<String, PathBuf>, 68 ) -> Result<Vec<Module>, Error> { 69 let span = tracing::info_span!("compile", package = %self.config.name.as_str()); 70 let _enter = span.enter(); 71 72 tracing::info!("Parsing source code"); 73 let parsed_modules = parse_sources( 74 &self.config.name, 75 std::mem::take(&mut self.sources), 76 already_defined_modules, 77 )?; 78 79 // Determine order in which modules are to be processed 80 let sequence = dep_tree::toposort_deps( 81 parsed_modules 82 .values() 83 .map(|m| module_deps_for_graph(self.target, m)) 84 .collect(), 85 ) 86 .map_err(convert_deps_tree_error)?; 87 88 tracing::info!("Type checking modules"); 89 let mut modules = type_check( 90 &self.config.name, 91 self.target, 92 sequence, 93 parsed_modules, 94 existing_modules, 95 warnings, 96 )?; 97 98 tracing::info!("Performing code generation"); 99 self.perform_codegen(&modules)?; 100 101 self.encode_and_write_metadata(&modules)?; 102 103 Ok(modules) 104 } 105 106 fn compile_erlang_to_beam(&self, modules: &[PathBuf]) -> Result<(), Error> { 107 tracing::info!("compiling_erlang"); 108 109 let env = [ 110 ("ERL_LIBS", self.erl_libs.to_string()), 111 ("TERM", "dumb".into()), 112 ]; 113 let mut args = vec![ 114 // Use a compile server to avoid repeatedly starting VM 115 "-server".into(), 116 // Write compiled .beam to ./ebin 117 "-o".into(), 118 self.out.join("ebin").to_string_lossy().to_string(), 119 ]; 120 // Add the list of modules to compile 121 for module in modules { 122 args.push( 123 self.out 124 .join("build") 125 .join(module) 126 .to_string_lossy() 127 .to_string(), 128 ); 129 } 130 let status = self.io.exec("erlc", &args, &env, None)?; 131 132 if status == 0 { 133 Ok(()) 134 } else { 135 Err(Error::ShellCommand { 136 command: "erlc".to_string(), 137 err: None, 138 }) 139 } 140 } 141 142 fn copy_project_erlang_files(&mut self, modules: &mut Vec<PathBuf>) -> Result<(), Error> { 143 tracing::info!("copying_erlang_source_files"); 144 let src = self.root.join("src"); 145 let test = self.root.join("test"); 146 let mut copied = HashSet::new(); 147 self.copy_erlang_files(&src, &mut copied, modules)?; 148 if self.io.is_directory(&test) { 149 self.copy_erlang_files(&test, &mut copied, modules)?; 150 } 151 Ok(()) 152 } 153 154 fn copy_erlang_files( 155 &self, 156 src_path: &Path, 157 copied: &mut HashSet<PathBuf>, 158 to_compile_modules: &mut Vec<PathBuf>, 159 ) -> Result<()> { 160 let out = self.out.join("build"); 161 self.io.mkdir(&out)?; 162 163 for entry in self.io.read_dir(src_path)? { 164 let path = entry.expect("copy_erlang_files dir_entry").path(); 165 166 let extension = path 167 .extension() 168 .unwrap_or_default() 169 .to_str() 170 .unwrap_or_default(); 171 let relative_path = path 172 .strip_prefix(src_path) 173 .expect("copy_erlang_files strip prefix") 174 .to_path_buf(); 175 176 match extension { 177 "hrl" => (), 178 "erl" => { 179 to_compile_modules.push(relative_path.clone()); 180 } 181 _ => continue, 182 }; 183 184 self.io.copy(&path, &out.join(&relative_path))?; 185 186 // TODO: test 187 if !copied.insert(relative_path.clone()) { 188 return Err(Error::DuplicateErlangFile { 189 file: relative_path.to_string_lossy().to_string(), 190 }); 191 } 192 } 193 Ok(()) 194 } 195 196 fn encode_and_write_metadata(&mut self, modules: &[Module]) -> Result<()> { 197 if !self.write_metadata { 198 tracing::info!("Package metadata writing disabled"); 199 return Ok(()); 200 } 201 tracing::info!("Writing package metadata to disc"); 202 for module in modules { 203 let name = format!("{}.gleam_module", &module.name.replace('/', "@")); 204 let path = self.out.join("build").join(name); 205 ModuleEncoder::new(&module.ast.type_info).write(self.io.writer(&path)?)?; 206 } 207 Ok(()) 208 } 209 210 pub fn read_source_files(&mut self) -> Result<()> { 211 let span = tracing::info_span!("load", package = %self.config.name.as_str()); 212 let _enter = span.enter(); 213 tracing::info!("Reading source files"); 214 let src = self.root.join("src"); 215 let test = self.root.join("test"); 216 217 // Src 218 for path in self.io.gleam_source_files(&src) { 219 self.add_module(path, &src, Origin::Src)?; 220 } 221 222 // Test 223 if self.io.is_directory(&test) { 224 for path in self.io.gleam_source_files(&test) { 225 self.add_module(path, &test, Origin::Test)?; 226 } 227 } 228 Ok(()) 229 } 230 231 fn add_module(&mut self, path: PathBuf, dir: &Path, origin: Origin) -> Result<()> { 232 let name = module_name(&dir, &path); 233 let code = self.io.read(&path)?; 234 self.sources.push(Source { 235 name, 236 path, 237 code, 238 origin, 239 }); 240 Ok(()) 241 } 242 243 fn perform_codegen(&mut self, modules: &[Module]) -> Result<()> { 244 let artifact_dir = self.out.join("build"); 245 match self.target { 246 Target::JavaScript => { 247 JavaScript::new(&artifact_dir).render(&self.io, modules)?; 248 } 249 250 Target::Erlang => { 251 let io = self.io.clone(); 252 Erlang::new(&artifact_dir).render(io.clone(), modules)?; 253 ErlangApp::new(&self.out.join("ebin")).render(io, &self.config, modules)?; 254 let mut erlang = vec![]; 255 256 if self.write_entrypoint { 257 self.render_entrypoint_module(&artifact_dir, &mut erlang)?; 258 } 259 260 if self.compile_erlang { 261 erlang.extend(modules.iter().map(Module::compiled_erlang_path)); 262 self.copy_project_erlang_files(&mut erlang)?; 263 self.compile_erlang_to_beam(&erlang)?; 264 } 265 } 266 } 267 Ok(()) 268 } 269 270 fn render_entrypoint_module( 271 &self, 272 out: &Path, 273 modules_to_compile: &mut Vec<PathBuf>, 274 ) -> Result<(), Error> { 275 let name = "gleam@@main.erl"; 276 let module = ErlangEntrypointModule { 277 application: &self.config.name, 278 } 279 .render() 280 .expect("Erlang entrypoint rendering"); 281 self.io.writer(&out.join(name))?.write(module.as_bytes())?; 282 modules_to_compile.push(name.into()); 283 Ok(()) 284 } 285} 286 287fn type_check( 288 package_name: &str, 289 target: Target, 290 sequence: Vec<String>, 291 mut parsed_modules: HashMap<String, Parsed>, 292 module_types: &mut HashMap<String, type_::Module>, 293 warnings: &mut Vec<Warning>, 294) -> Result<Vec<Module>, Error> { 295 let mut modules = Vec::with_capacity(parsed_modules.len() + 1); 296 let mut uid = 0; 297 298 // Insert the prelude 299 // DUPE: preludeinsertion 300 // TODO: Currently we do this here and also in the tests. It would be better 301 // to have one place where we create all this required state for use in each 302 // place. 303 let _ = module_types.insert("gleam".to_string(), type_::build_prelude(&mut uid)); 304 305 for name in sequence { 306 let Parsed { 307 name, 308 code, 309 ast, 310 path, 311 origin, 312 package, 313 extra, 314 } = parsed_modules 315 .remove(&name) 316 .expect("Getting parsed module for name"); 317 318 tracing::debug!(module = ?name, "Type checking"); 319 let mut type_warnings = Vec::new(); 320 let ast = type_::infer_module( 321 target, 322 &mut uid, 323 ast, 324 origin, 325 package_name, 326 module_types, 327 &mut type_warnings, 328 ) 329 .map_err(|error| Error::Type { 330 path: path.clone(), 331 src: code.clone(), 332 error, 333 })?; 334 335 // Register any warnings emitted as type warnings 336 let type_warnings = type_warnings 337 .into_iter() 338 .map(|w| w.into_warning(path.clone(), code.clone())); 339 warnings.extend(type_warnings); 340 341 // Register the types from this module so they can be imported into 342 // other modules. 343 let _ = module_types.insert(name.clone(), ast.type_info.clone()); 344 345 // Register the successfully type checked module data so that it can be 346 // used for code generation 347 modules.push(Module { 348 origin, 349 extra, 350 name, 351 code, 352 ast, 353 input_path: path, 354 }); 355 } 356 357 Ok(modules) 358} 359 360fn convert_deps_tree_error(e: dep_tree::Error) -> Error { 361 match e { 362 dep_tree::Error::Cycle(modules) => Error::ImportCycle { modules }, 363 } 364} 365 366fn module_deps_for_graph(target: Target, module: &Parsed) -> (String, Vec<String>) { 367 let name = module.name.clone(); 368 let deps: Vec<_> = module 369 .ast 370 .dependencies(target) 371 .into_iter() 372 .map(|(dep, _span)| dep) 373 .collect(); 374 (name, deps) 375} 376 377fn parse_sources( 378 package_name: &str, 379 sources: Vec<Source>, 380 already_defined_modules: &mut HashMap<String, PathBuf>, 381) -> Result<HashMap<String, Parsed>, Error> { 382 let mut parsed_modules = HashMap::with_capacity(sources.len()); 383 for Source { 384 name, 385 code, 386 path, 387 origin, 388 } in sources 389 { 390 let (mut ast, extra) = crate::parse::parse_module(&code).map_err(|error| Error::Parse { 391 path: path.clone(), 392 src: code.clone(), 393 error, 394 })?; 395 396 // Store the name 397 ast.name = name.split("/").map(String::from).collect(); // TODO: store the module name as a string 398 399 let module = Parsed { 400 package: package_name.to_string(), 401 origin, 402 extra, 403 path, 404 name, 405 code, 406 ast, 407 }; 408 409 // Ensure there are no modules defined that already have this name 410 if let Some(first) = 411 already_defined_modules.insert(module.name.clone(), module.path.clone()) 412 { 413 return Err(Error::DuplicateModule { 414 module: module.name.clone(), 415 first, 416 second: module.path.clone(), 417 }); 418 } 419 420 // Register the parsed module 421 let _ = parsed_modules.insert(module.name.clone(), module); 422 } 423 Ok(parsed_modules) 424} 425 426fn module_name(package_path: &Path, full_module_path: &Path) -> String { 427 // /path/to/project/_build/default/lib/the_package/src/my/module.gleam 428 429 // my/module.gleam 430 let mut module_path = full_module_path 431 .strip_prefix(package_path) 432 .expect("Stripping package prefix from module path") 433 .to_path_buf(); 434 435 // my/module 436 let _ = module_path.set_extension(""); 437 438 // Stringify 439 let name = module_path 440 .to_str() 441 .expect("Module name path to str") 442 .to_string(); 443 444 // normalise windows paths 445 name.replace("\\", "/") 446} 447 448#[derive(Debug)] 449pub struct Source { 450 pub path: PathBuf, 451 pub name: String, 452 pub code: String, 453 pub origin: Origin, // TODO: is this used? 454} 455 456#[derive(Debug)] 457struct Parsed { 458 path: PathBuf, 459 name: String, 460 code: String, 461 origin: Origin, 462 package: String, 463 ast: UntypedModule, 464 extra: ModuleExtra, 465} 466 467#[derive(Template)] 468#[template(path = "gleam@@main.erl", escape = "none")] 469struct ErlangEntrypointModule<'a> { 470 application: &'a str, 471}