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
9.6 kB 300 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 native_modules: Vec<EcoString>, 103 ) -> Result<()> { 104 fn tuple(key: &str, value: &str) -> String { 105 format!(" {{{key}, {value}}},\n") 106 } 107 108 let path = self.output_directory.join(format!("{}.app", &config.name)); 109 110 let start_module = match config.erlang.application_start_module.as_ref() { 111 None => "".into(), 112 Some(module) => { 113 let module = module_erlang_name(module); 114 let argument = match config.erlang.application_start_argument.as_ref() { 115 Some(argument) => argument.as_str(), 116 None => "[]", 117 }; 118 tuple("mod", &format!("{{'{module}', {argument}}}")) 119 } 120 }; 121 122 let modules = modules 123 .iter() 124 .map(|m| m.erlang_name()) 125 .chain(native_modules) 126 .unique() 127 .sorted() 128 .map(escape_atom_string) 129 .join(",\n "); 130 131 // TODO: When precompiling for production (i.e. as a precompiled hex 132 // package) we will need to exclude the dev deps. 133 let applications = config 134 .dependencies 135 .keys() 136 .chain( 137 config 138 .dev_dependencies 139 .keys() 140 .take_while(|_| self.config.include_dev_deps), 141 ) 142 // TODO: test this! 143 .map(|name| self.config.package_name_overrides.get(name).unwrap_or(name)) 144 .chain(config.erlang.extra_applications.iter()) 145 .sorted() 146 .join(",\n "); 147 148 let text = format!( 149 r#"{{application, {package}, [ 150{start_module} {{vsn, "{version}"}}, 151 {{applications, [{applications}]}}, 152 {{description, "{description}"}}, 153 {{modules, [{modules}]}}, 154 {{registered, []}} 155]}}. 156"#, 157 applications = applications, 158 description = config.description, 159 modules = modules, 160 package = config.name, 161 start_module = start_module, 162 version = config.version, 163 ); 164 165 writer.write(&path, &text) 166 } 167} 168 169#[derive(Debug, Clone, Copy, PartialEq, Eq)] 170pub enum TypeScriptDeclarations { 171 None, 172 Emit, 173} 174 175#[derive(Debug)] 176pub struct JavaScript<'a> { 177 output_directory: &'a Utf8Path, 178 prelude_location: &'a Utf8Path, 179 project_root: &'a Utf8Path, 180 typescript: TypeScriptDeclarations, 181 source_map: bool, 182} 183 184impl<'a> JavaScript<'a> { 185 pub fn new( 186 output_directory: &'a Utf8Path, 187 typescript: TypeScriptDeclarations, 188 source_map: bool, 189 prelude_location: &'a Utf8Path, 190 project_root: &'a Utf8Path, 191 ) -> Self { 192 Self { 193 prelude_location, 194 output_directory, 195 project_root, 196 typescript, 197 source_map, 198 } 199 } 200 201 pub fn render( 202 &self, 203 writer: &impl FileSystemWriter, 204 modules: &[Module], 205 stdlib_package: StdlibPackage, 206 ) -> Result<()> { 207 for module in modules { 208 let js_name = module.name.clone(); 209 if self.typescript == TypeScriptDeclarations::Emit { 210 self.ts_declaration(writer, module, &js_name)?; 211 } 212 self.js_module(writer, module, &js_name, stdlib_package)? 213 } 214 self.write_prelude(writer)?; 215 Ok(()) 216 } 217 218 fn write_prelude(&self, writer: &impl FileSystemWriter) -> Result<()> { 219 let rexport = format!("export * from \"{}\";\n", self.prelude_location); 220 let prelude_path = &self.output_directory.join("gleam.mjs"); 221 222 // This check skips unnecessary `gleam.mjs` writes which confuse 223 // watchers and HMR build tools 224 if !writer.exists(prelude_path) { 225 writer.write(prelude_path, &rexport)?; 226 } 227 228 if self.typescript == TypeScriptDeclarations::Emit { 229 let rexport = format!( 230 "export * from \"{}\";\nexport type * from \"{}\";\n", 231 self.prelude_location, 232 self.prelude_location.as_str().replace(".mjs", ".d.mts") 233 ); 234 let prelude_declaration_path = &self.output_directory.join("gleam.d.mts"); 235 236 // Type declaration may trigger badly configured watchers 237 if !writer.exists(prelude_declaration_path) { 238 writer.write(prelude_declaration_path, &rexport)?; 239 } 240 } 241 242 Ok(()) 243 } 244 245 fn ts_declaration( 246 &self, 247 writer: &impl FileSystemWriter, 248 module: &Module, 249 js_name: &str, 250 ) -> Result<()> { 251 let name = format!("{js_name}.d.mts"); 252 let path = self.output_directory.join(name); 253 let output = javascript::ts_declaration(&module.ast); 254 tracing::debug!(name = ?js_name, "Generated TS declaration"); 255 writer.write(&path, &output) 256 } 257 258 fn js_module( 259 &self, 260 writer: &impl FileSystemWriter, 261 module: &Module, 262 js_name: &str, 263 stdlib_package: StdlibPackage, 264 ) -> Result<()> { 265 let name = format!("{js_name}.mjs"); 266 let path = self.output_directory.join(name); 267 let line_numbers = LineNumbers::new(&module.code); 268 let (output, source_map) = javascript::module(ModuleConfig { 269 module: &module.ast, 270 line_numbers: &line_numbers, 271 path: &module.input_path, 272 project_root: self.project_root, 273 src: &module.code, 274 typescript: self.typescript, 275 source_map: self.source_map, 276 stdlib_package, 277 }); 278 tracing::debug!(name = ?js_name, "Generated js module"); 279 writer.write(&path, &output)?; 280 281 if let Some(source_map) = source_map { 282 let mut output = Vec::new(); 283 // We first write to a vector then build a string, hoping that 284 // the `sourcemap` crate generated a valid sourcemap. If it 285 // did not, it is a bug that should be reported. 286 // 287 // SourceMap currently does not support being written directly 288 // to a string. 289 source_map 290 .to_writer(&mut output) 291 .expect("Failed to write sourcemap to memory."); 292 let content = 293 String::from_utf8(output).expect("Sourcemap did not generate valid UTF-8."); 294 let source_map_path = self.output_directory.join(format!("{js_name}.mjs.map")); 295 tracing::debug!(path = ?source_map_path, name = ?js_name, "Emitting sourcemap for module"); 296 writer.write(&source_map_path, &content)?; 297 } 298 Ok(()) 299 } 300}