Fork of daniellemaywood.uk/gleam — Wasm codegen work
Something went wrong. Try again.
12 kB · 370 lines
Rust
at wasm
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371// SPDX-License-Identifier: Apache-2.0// SPDX-FileCopyrightText: 2020 The Gleam contributors
use crate::{ Result, build::{ ErlangAppCodegenConfiguration, Module, module_erlang_name, package_compiler::StdlibPackage, }, config::PackageConfig, erlang, io::FileSystemWriter, javascript::{self, ModuleConfig}, line_numbers::LineNumbers, wasm,};use ecow::EcoString;use erlang::escape_atom_string;use itertools::Itertools;use std::fmt::Debug;
use camino::Utf8Path;
/// A code generator that creates a .erl Erlang module and record header files/// for each Gleam module in the package.#[derive(Debug)]pub struct Erlang<'a> { build_directory: &'a Utf8Path, include_directory: &'a Utf8Path,}
impl<'a> Erlang<'a> { pub fn new(build_directory: &'a Utf8Path, include_directory: &'a Utf8Path) -> Self { Self { build_directory, include_directory, } }
pub fn render<Writer: FileSystemWriter>( &self, writer: Writer, modules: &[Module], root: &Utf8Path, ) -> Result<()> { for module in modules { let erl_name = module.erlang_name(); self.erlang_module(&writer, module, &erl_name, root)?; self.erlang_record_headers(&writer, module, &erl_name)?; } Ok(()) }
fn erlang_module<Writer: FileSystemWriter>( &self, writer: &Writer, module: &Module, erl_name: &str, root: &Utf8Path, ) -> Result<()> { let name = format!("{erl_name}.erl"); let path = self.build_directory.join(&name); let line_numbers = LineNumbers::new(&module.code); let output = erlang::module(&module.ast, &line_numbers, root); tracing::debug!(name = ?name, "Generated Erlang module"); writer.write(&path, &output) }
fn erlang_record_headers<Writer: FileSystemWriter>( &self, writer: &Writer, module: &Module, erl_name: &str, ) -> Result<()> { for (name, text) in erlang::records(&module.ast) { let name = format!("{erl_name}_{name}.hrl"); tracing::debug!(name = ?name, "Generated Erlang header"); writer.write(&self.include_directory.join(name), &text)?; } Ok(()) }}
/// A code generator that creates a .app Erlang application file for the package#[derive(Debug)]pub struct ErlangApp<'a> { output_directory: &'a Utf8Path, config: &'a ErlangAppCodegenConfiguration,}
impl<'a> ErlangApp<'a> { pub fn new(output_directory: &'a Utf8Path, config: &'a ErlangAppCodegenConfiguration) -> Self { Self { output_directory, config, } }
pub fn render<Writer: FileSystemWriter>( &self, writer: Writer, config: &PackageConfig, modules: &[Module], cached_module_names: &[EcoString], native_modules: Vec<EcoString>, ) -> Result<()> { fn tuple(key: &str, value: &str) -> String { format!(" {{{key}, {value}}},\n") }
let path = self.output_directory.join(format!("{}.app", config.name));
let start_module = match config.erlang.application_start_module.as_ref() { None => "".into(), Some(module) => { let module = module_erlang_name(module); let argument = match config.erlang.application_start_argument.as_ref() { Some(argument) => argument.as_str(), None => "[]", }; tuple("mod", &format!("{{'{module}', {argument}}}")) } };
// Include the cached modules that were not recompiled this build, so a // warm rebuild doesn't shrink the `modules` list (see #5834). let modules = modules .iter() .map(|m| m.erlang_name()) .chain(cached_module_names.iter().map(module_erlang_name)) .chain(native_modules) .unique() .sorted() .map(escape_atom_string) .join(",\n ");
// TODO: When precompiling for production (i.e. as a precompiled hex // package) we will need to exclude the dev deps. let applications = config .dependencies .keys() .chain( config .dev_dependencies .keys() .take_while(|_| self.config.include_dev_deps), ) // TODO: test this! .map(|name| self.config.package_name_overrides.get(name).unwrap_or(name)) .chain(config.erlang.extra_applications.iter()) .sorted() .join(",\n ");
let text = format!( r#"{{application, {package}, [{start_module} {{vsn, "{version}"}}, {{applications, [{applications}]}}, {{description, "{description}"}}, {{modules, [{modules}]}}, {{registered, []}}]}}."#, applications = applications, description = config.description, modules = modules, package = config.name, start_module = start_module, version = config.version, );
writer.write(&path, &text) }}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum TypeScriptDeclarations { None, Emit,}
#[derive(Debug)]pub struct JavaScript<'a> { output_directory: &'a Utf8Path, prelude_location: &'a Utf8Path, project_root: &'a Utf8Path, typescript: TypeScriptDeclarations, source_map: bool,}
impl<'a> JavaScript<'a> { pub fn new( output_directory: &'a Utf8Path, typescript: TypeScriptDeclarations, source_map: bool, prelude_location: &'a Utf8Path, project_root: &'a Utf8Path, ) -> Self { Self { prelude_location, output_directory, project_root, typescript, source_map, } }
pub fn render( &self, writer: &impl FileSystemWriter, modules: &[Module], stdlib_package: StdlibPackage, ) -> Result<()> { for module in modules { let js_name = module.name.clone(); if self.typescript == TypeScriptDeclarations::Emit { self.ts_declaration(writer, module, &js_name)?; } self.js_module(writer, module, &js_name, stdlib_package)?; } self.write_prelude(writer)?; Ok(()) }
fn write_prelude(&self, writer: &impl FileSystemWriter) -> Result<()> { let rexport = format!("export * from \"{}\";\n", self.prelude_location); let prelude_path = &self.output_directory.join("gleam.mjs");
// This check skips unnecessary `gleam.mjs` writes which confuse // watchers and HMR build tools if !writer.exists(prelude_path) { writer.write(prelude_path, &rexport)?; }
if self.typescript == TypeScriptDeclarations::Emit { let rexport = format!( "export * from \"{}\";\nexport type * from \"{}\";\n", self.prelude_location, self.prelude_location.as_str().replace(".mjs", ".d.mts") ); let prelude_declaration_path = &self.output_directory.join("gleam.d.mts");
// Type declaration may trigger badly configured watchers if !writer.exists(prelude_declaration_path) { writer.write(prelude_declaration_path, &rexport)?; } }
Ok(()) }
fn ts_declaration( &self, writer: &impl FileSystemWriter, module: &Module, js_name: &str, ) -> Result<()> { let name = format!("{js_name}.d.mts"); let path = self.output_directory.join(name); let output = javascript::ts_declaration(&module.ast); tracing::debug!(name = ?js_name, "Generated TS declaration"); writer.write(&path, &output) }
fn js_module( &self, writer: &impl FileSystemWriter, module: &Module, js_name: &str, stdlib_package: StdlibPackage, ) -> Result<()> { let name = format!("{js_name}.mjs"); let path = self.output_directory.join(name); let line_numbers = LineNumbers::new(&module.code); let (output, source_map) = javascript::module(ModuleConfig { module: &module.ast, line_numbers: &line_numbers, path: &module.input_path, project_root: self.project_root, src: &module.code, typescript: self.typescript, source_map: self.source_map, stdlib_package, }); tracing::debug!(name = ?js_name, "Generated js module"); writer.write(&path, &output)?;
if let Some(source_map) = source_map { let mut output = Vec::new(); // We first write to a vector then build a string, hoping that // the `sourcemap` crate generated a valid sourcemap. If it // did not, it is a bug that should be reported. // // SourceMap currently does not support being written directly // to a string. source_map .to_writer(&mut output) .expect("Failed to write sourcemap to memory."); let content = String::from_utf8(output).expect("Sourcemap did not generate valid UTF-8."); let source_map_path = self.output_directory.join(format!("{js_name}.mjs.map")); tracing::debug!(path = ?source_map_path, name = ?js_name, "Emitting sourcemap for module"); writer.write(&source_map_path, &content)?; } Ok(()) }}
#[cfg(test)]mod tests { use super::*; use crate::io::{FileSystemReader, memory::InMemoryFileSystem}; use std::collections::HashMap;
#[test] fn app_file_includes_cached_modules() { // A warm rebuild recompiles nothing, so every module arrives as a cached // name rather than in `modules`; they must still be listed in the .app // (https://github.com/gleam-lang/gleam/issues/5834). let fs = InMemoryFileSystem::new(); let mut config = PackageConfig::default(); config.name = "my_app".into(); let codegen_config = ErlangAppCodegenConfiguration { include_dev_deps: false, package_name_overrides: HashMap::new(), };
ErlangApp::new(Utf8Path::new("/ebin"), &codegen_config) .render( fs.clone(), &config, &[], &["my_app/one".into(), "my_app/two".into()], vec![], ) .expect("render .app");
let app = fs .read(Utf8Path::new("/ebin/my_app.app")) .expect("read .app"); insta::assert_snapshot!(app); }}
#[derive(Debug)]pub struct Wasm<'a> { output_directory: &'a Utf8Path,}
impl<'a> Wasm<'a> { pub fn new(output_directory: &'a Utf8Path) -> Self { Self { output_directory } }
pub fn render(&self, writer: &impl FileSystemWriter, modules: &[Module]) -> Result<()> { let modules: Vec<_> = modules .iter() .map(|module| wasm::mir::Translator::new(&module.ast).translate()) .collect();
let monomorphized = wasm::mir::lower::lower(modules); let linked = wasm::link_package(monomorphized);
let path = self .output_directory .join(format!("{}.wasm", linked.name)); let object = wasm::compile(linked); writer.write_bytes(&path, &object)?;
Ok(()) }}