Fork of daniellemaywood.uk/gleam — Wasm codegen work
12 kB
364 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 cranelift, 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}
341
342#[derive(Debug)]
343pub struct Cranelift<'a> {
344 output_directory: &'a Utf8Path,
345}
346
347impl<'a> Cranelift<'a> {
348 pub fn new(output_directory: &'a Utf8Path) -> Self {
349 Self { output_directory }
350 }
351
352 pub fn render(&self, writer: &impl FileSystemWriter, modules: &[Module]) -> Result<()> {
353 _ = self.output_directory;
354 _ = writer;
355
356 for module in modules {
357 let mir_module = cranelift::mir::Translator::default().translate(&module.ast);
358
359 cranelift::compile(mir_module);
360 }
361
362 Ok(())
363 }
364}