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