Fork of daniellemaywood.uk/gleam — Wasm codegen work
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}