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 / codegen.rs
12 kB 370 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2020 The Gleam contributors 3 4use crate::{ 5 Result, 6 build::{ 7 ErlangAppCodegenConfiguration, Module, module_erlang_name, package_compiler::StdlibPackage, 8 }, 9 config::PackageConfig, 10 erlang, 11 io::FileSystemWriter, 12 javascript::{self, ModuleConfig}, 13 line_numbers::LineNumbers, 14 wasm, 15}; 16use ecow::EcoString; 17use erlang::escape_atom_string; 18use itertools::Itertools; 19use std::fmt::Debug; 20 21use camino::Utf8Path; 22 23/// A code generator that creates a .erl Erlang module and record header files 24/// for each Gleam module in the package. 25#[derive(Debug)] 26pub struct Erlang<'a> { 27 build_directory: &'a Utf8Path, 28 include_directory: &'a Utf8Path, 29} 30 31impl<'a> Erlang<'a> { 32 pub fn new(build_directory: &'a Utf8Path, include_directory: &'a Utf8Path) -> Self { 33 Self { 34 build_directory, 35 include_directory, 36 } 37 } 38 39 pub fn render<Writer: FileSystemWriter>( 40 &self, 41 writer: Writer, 42 modules: &[Module], 43 root: &Utf8Path, 44 ) -> Result<()> { 45 for module in modules { 46 let erl_name = module.erlang_name(); 47 self.erlang_module(&writer, module, &erl_name, root)?; 48 self.erlang_record_headers(&writer, module, &erl_name)?; 49 } 50 Ok(()) 51 } 52 53 fn erlang_module<Writer: FileSystemWriter>( 54 &self, 55 writer: &Writer, 56 module: &Module, 57 erl_name: &str, 58 root: &Utf8Path, 59 ) -> Result<()> { 60 let name = format!("{erl_name}.erl"); 61 let path = self.build_directory.join(&name); 62 let line_numbers = LineNumbers::new(&module.code); 63 let output = erlang::module(&module.ast, &line_numbers, root); 64 tracing::debug!(name = ?name, "Generated Erlang module"); 65 writer.write(&path, &output) 66 } 67 68 fn erlang_record_headers<Writer: FileSystemWriter>( 69 &self, 70 writer: &Writer, 71 module: &Module, 72 erl_name: &str, 73 ) -> Result<()> { 74 for (name, text) in erlang::records(&module.ast) { 75 let name = format!("{erl_name}_{name}.hrl"); 76 tracing::debug!(name = ?name, "Generated Erlang header"); 77 writer.write(&self.include_directory.join(name), &text)?; 78 } 79 Ok(()) 80 } 81} 82 83/// A code generator that creates a .app Erlang application file for the package 84#[derive(Debug)] 85pub struct ErlangApp<'a> { 86 output_directory: &'a Utf8Path, 87 config: &'a ErlangAppCodegenConfiguration, 88} 89 90impl<'a> ErlangApp<'a> { 91 pub fn new(output_directory: &'a Utf8Path, config: &'a ErlangAppCodegenConfiguration) -> Self { 92 Self { 93 output_directory, 94 config, 95 } 96 } 97 98 pub fn render<Writer: FileSystemWriter>( 99 &self, 100 writer: Writer, 101 config: &PackageConfig, 102 modules: &[Module], 103 cached_module_names: &[EcoString], 104 native_modules: Vec<EcoString>, 105 ) -> Result<()> { 106 fn tuple(key: &str, value: &str) -> String { 107 format!(" {{{key}, {value}}},\n") 108 } 109 110 let path = self.output_directory.join(format!("{}.app", config.name)); 111 112 let start_module = match config.erlang.application_start_module.as_ref() { 113 None => "".into(), 114 Some(module) => { 115 let module = module_erlang_name(module); 116 let argument = match config.erlang.application_start_argument.as_ref() { 117 Some(argument) => argument.as_str(), 118 None => "[]", 119 }; 120 tuple("mod", &format!("{{'{module}', {argument}}}")) 121 } 122 }; 123 124 // Include the cached modules that were not recompiled this build, so a 125 // warm rebuild doesn't shrink the `modules` list (see #5834). 126 let modules = modules 127 .iter() 128 .map(|m| m.erlang_name()) 129 .chain(cached_module_names.iter().map(module_erlang_name)) 130 .chain(native_modules) 131 .unique() 132 .sorted() 133 .map(escape_atom_string) 134 .join(",\n "); 135 136 // TODO: When precompiling for production (i.e. as a precompiled hex 137 // package) we will need to exclude the dev deps. 138 let applications = config 139 .dependencies 140 .keys() 141 .chain( 142 config 143 .dev_dependencies 144 .keys() 145 .take_while(|_| self.config.include_dev_deps), 146 ) 147 // TODO: test this! 148 .map(|name| self.config.package_name_overrides.get(name).unwrap_or(name)) 149 .chain(config.erlang.extra_applications.iter()) 150 .sorted() 151 .join(",\n "); 152 153 let text = format!( 154 r#"{{application, {package}, [ 155{start_module} {{vsn, "{version}"}}, 156 {{applications, [{applications}]}}, 157 {{description, "{description}"}}, 158 {{modules, [{modules}]}}, 159 {{registered, []}} 160]}}. 161"#, 162 applications = applications, 163 description = config.description, 164 modules = modules, 165 package = config.name, 166 start_module = start_module, 167 version = config.version, 168 ); 169 170 writer.write(&path, &text) 171 } 172} 173 174#[derive(Debug, Clone, Copy, PartialEq, Eq)] 175pub enum TypeScriptDeclarations { 176 None, 177 Emit, 178} 179 180#[derive(Debug)] 181pub struct JavaScript<'a> { 182 output_directory: &'a Utf8Path, 183 prelude_location: &'a Utf8Path, 184 project_root: &'a Utf8Path, 185 typescript: TypeScriptDeclarations, 186 source_map: bool, 187} 188 189impl<'a> JavaScript<'a> { 190 pub fn new( 191 output_directory: &'a Utf8Path, 192 typescript: TypeScriptDeclarations, 193 source_map: bool, 194 prelude_location: &'a Utf8Path, 195 project_root: &'a Utf8Path, 196 ) -> Self { 197 Self { 198 prelude_location, 199 output_directory, 200 project_root, 201 typescript, 202 source_map, 203 } 204 } 205 206 pub fn render( 207 &self, 208 writer: &impl FileSystemWriter, 209 modules: &[Module], 210 stdlib_package: StdlibPackage, 211 ) -> Result<()> { 212 for module in modules { 213 let js_name = module.name.clone(); 214 if self.typescript == TypeScriptDeclarations::Emit { 215 self.ts_declaration(writer, module, &js_name)?; 216 } 217 self.js_module(writer, module, &js_name, stdlib_package)?; 218 } 219 self.write_prelude(writer)?; 220 Ok(()) 221 } 222 223 fn write_prelude(&self, writer: &impl FileSystemWriter) -> Result<()> { 224 let rexport = format!("export * from \"{}\";\n", self.prelude_location); 225 let prelude_path = &self.output_directory.join("gleam.mjs"); 226 227 // This check skips unnecessary `gleam.mjs` writes which confuse 228 // watchers and HMR build tools 229 if !writer.exists(prelude_path) { 230 writer.write(prelude_path, &rexport)?; 231 } 232 233 if self.typescript == TypeScriptDeclarations::Emit { 234 let rexport = format!( 235 "export * from \"{}\";\nexport type * from \"{}\";\n", 236 self.prelude_location, 237 self.prelude_location.as_str().replace(".mjs", ".d.mts") 238 ); 239 let prelude_declaration_path = &self.output_directory.join("gleam.d.mts"); 240 241 // Type declaration may trigger badly configured watchers 242 if !writer.exists(prelude_declaration_path) { 243 writer.write(prelude_declaration_path, &rexport)?; 244 } 245 } 246 247 Ok(()) 248 } 249 250 fn ts_declaration( 251 &self, 252 writer: &impl FileSystemWriter, 253 module: &Module, 254 js_name: &str, 255 ) -> Result<()> { 256 let name = format!("{js_name}.d.mts"); 257 let path = self.output_directory.join(name); 258 let output = javascript::ts_declaration(&module.ast); 259 tracing::debug!(name = ?js_name, "Generated TS declaration"); 260 writer.write(&path, &output) 261 } 262 263 fn js_module( 264 &self, 265 writer: &impl FileSystemWriter, 266 module: &Module, 267 js_name: &str, 268 stdlib_package: StdlibPackage, 269 ) -> Result<()> { 270 let name = format!("{js_name}.mjs"); 271 let path = self.output_directory.join(name); 272 let line_numbers = LineNumbers::new(&module.code); 273 let (output, source_map) = javascript::module(ModuleConfig { 274 module: &module.ast, 275 line_numbers: &line_numbers, 276 path: &module.input_path, 277 project_root: self.project_root, 278 src: &module.code, 279 typescript: self.typescript, 280 source_map: self.source_map, 281 stdlib_package, 282 }); 283 tracing::debug!(name = ?js_name, "Generated js module"); 284 writer.write(&path, &output)?; 285 286 if let Some(source_map) = source_map { 287 let mut output = Vec::new(); 288 // We first write to a vector then build a string, hoping that 289 // the `sourcemap` crate generated a valid sourcemap. If it 290 // did not, it is a bug that should be reported. 291 // 292 // SourceMap currently does not support being written directly 293 // to a string. 294 source_map 295 .to_writer(&mut output) 296 .expect("Failed to write sourcemap to memory."); 297 let content = 298 String::from_utf8(output).expect("Sourcemap did not generate valid UTF-8."); 299 let source_map_path = self.output_directory.join(format!("{js_name}.mjs.map")); 300 tracing::debug!(path = ?source_map_path, name = ?js_name, "Emitting sourcemap for module"); 301 writer.write(&source_map_path, &content)?; 302 } 303 Ok(()) 304 } 305} 306 307#[cfg(test)] 308mod tests { 309 use super::*; 310 use crate::io::{FileSystemReader, memory::InMemoryFileSystem}; 311 use std::collections::HashMap; 312 313 #[test] 314 fn app_file_includes_cached_modules() { 315 // A warm rebuild recompiles nothing, so every module arrives as a cached 316 // name rather than in `modules`; they must still be listed in the .app 317 // (https://github.com/gleam-lang/gleam/issues/5834). 318 let fs = InMemoryFileSystem::new(); 319 let mut config = PackageConfig::default(); 320 config.name = "my_app".into(); 321 let codegen_config = ErlangAppCodegenConfiguration { 322 include_dev_deps: false, 323 package_name_overrides: HashMap::new(), 324 }; 325 326 ErlangApp::new(Utf8Path::new("/ebin"), &codegen_config) 327 .render( 328 fs.clone(), 329 &config, 330 &[], 331 &["my_app/one".into(), "my_app/two".into()], 332 vec![], 333 ) 334 .expect("render .app"); 335 336 let app = fs 337 .read(Utf8Path::new("/ebin/my_app.app")) 338 .expect("read .app"); 339 insta::assert_snapshot!(app); 340 } 341} 342 343#[derive(Debug)] 344pub struct Wasm<'a> { 345 output_directory: &'a Utf8Path, 346} 347 348impl<'a> Wasm<'a> { 349 pub fn new(output_directory: &'a Utf8Path) -> Self { 350 Self { output_directory } 351 } 352 353 pub fn render(&self, writer: &impl FileSystemWriter, modules: &[Module]) -> Result<()> { 354 let modules: Vec<_> = modules 355 .iter() 356 .map(|module| wasm::mir::Translator::new(&module.ast).translate()) 357 .collect(); 358 359 let monomorphized = wasm::mir::lower::lower(modules); 360 let linked = wasm::link_package(monomorphized); 361 362 let path = self 363 .output_directory 364 .join(format!("{}.wasm", linked.name)); 365 let object = wasm::compile(linked); 366 writer.write_bytes(&path, &object)?; 367 368 Ok(()) 369 } 370}