Fork of daniellemaywood.uk/gleam — Wasm codegen work
6.5 kB
223 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2021 The Gleam contributors
3
4#[cfg(test)]
5mod tests;
6mod wasm_filesystem;
7
8use camino::Utf8PathBuf;
9use gleam_core::{
10 Error,
11 analyse::TargetSupport,
12 build::{
13 Mode, NullTelemetry, PackageCompiler, StaleTracker, Target, TargetCodegenConfiguration,
14 },
15 config::PackageConfig,
16 io::{FileSystemReader, FileSystemWriter},
17 uid::UniqueIdGenerator,
18 warning::{VectorWarningEmitterIO, WarningEmitter},
19};
20use hexpm::version::Version;
21use im::HashMap;
22use std::{cell::RefCell, collections::HashSet, rc::Rc};
23use wasm_filesystem::WasmFileSystem;
24
25use wasm_bindgen::prelude::*;
26
27#[derive(Debug, Clone, Default)]
28struct Project {
29 fs: WasmFileSystem,
30 warnings: VectorWarningEmitterIO,
31}
32
33thread_local! {
34 static PROJECTS: RefCell<HashMap<usize, Project>> = RefCell::new(HashMap::new());
35}
36
37/// You should call this once to ensure that if the compiler crashes it gets
38/// reported in JavaScript.
39///
40#[cfg(target_arch = "wasm32")]
41#[wasm_bindgen]
42pub fn initialise_panic_hook(debug: bool) {
43 console_error_panic_hook::set_once();
44
45 if debug {
46 let _ = tracing_wasm::try_set_as_global_default();
47 }
48}
49
50/// Reset the virtual file system to an empty state.
51///
52#[wasm_bindgen]
53pub fn reset_filesystem(project_id: usize) {
54 let fs = get_filesystem(project_id);
55 fs.reset();
56}
57
58/// Delete project, freeing any memory associated with it.
59///
60#[wasm_bindgen]
61pub fn delete_project(project_id: usize) {
62 PROJECTS.with(|lock| {
63 _ = lock.borrow_mut().remove(&project_id);
64 })
65}
66
67fn get_project(project_id: usize) -> Project {
68 PROJECTS.with(|lock| lock.borrow_mut().entry(project_id).or_default().clone())
69}
70
71fn get_filesystem(project_id: usize) -> WasmFileSystem {
72 get_project(project_id).fs
73}
74
75fn get_warnings(project_id: usize) -> VectorWarningEmitterIO {
76 get_project(project_id).warnings
77}
78
79/// Write a Gleam module to the `/src` directory of the virtual file system.
80///
81#[wasm_bindgen]
82pub fn write_module(project_id: usize, module_name: &str, code: &str) {
83 let fs = get_filesystem(project_id);
84 let path = format!("/src/{module_name}.gleam");
85 fs.write(&Utf8PathBuf::from(path), code)
86 .expect("writing file")
87}
88
89/// Write a file to the virtual file system.
90///
91#[wasm_bindgen]
92pub fn write_file(project_id: usize, path: &str, content: &str) {
93 let fs = get_filesystem(project_id);
94 fs.write(&Utf8PathBuf::from(path), content)
95 .expect("writing file")
96}
97
98/// Write a non-text file to the virtual file system.
99///
100#[wasm_bindgen]
101pub fn write_file_bytes(project_id: usize, path: &str, content: &[u8]) {
102 let fs = get_filesystem(project_id);
103 fs.write_bytes(&Utf8PathBuf::from(path), content)
104 .expect("writing file")
105}
106
107/// Read a file from the virtual file system.
108///
109#[wasm_bindgen]
110pub fn read_file_bytes(project_id: usize, path: &str) -> Option<Vec<u8>> {
111 let fs = get_filesystem(project_id);
112 fs.read_bytes(&Utf8PathBuf::from(path)).ok()
113}
114
115/// Run the package compiler. If this succeeds you can use
116///
117#[wasm_bindgen]
118pub fn compile_package(project_id: usize, target: &str) -> Result<(), String> {
119 let target = match target.to_lowercase().as_str() {
120 "erl" | "erlang" => Target::Erlang,
121 "js" | "javascript" => Target::JavaScript,
122 _ => {
123 let msg = format!("Unknown target `{target}`, expected `erlang` or `javascript`");
124 return Err(msg);
125 }
126 };
127
128 do_compile_package(get_project(project_id), target).map_err(|e| e.pretty_string())
129}
130
131/// Get the compiled JavaScript output for a given module.
132///
133/// You need to call `compile_package` before calling this function.
134///
135#[wasm_bindgen]
136pub fn read_compiled_javascript(project_id: usize, module_name: &str) -> Option<String> {
137 let fs = get_filesystem(project_id);
138 let path = format!("/build/{module_name}.mjs");
139 fs.read(&Utf8PathBuf::from(path)).ok()
140}
141
142/// Get the compiled Erlang output for a given module.
143///
144/// You need to call `compile_package` before calling this function.
145///
146#[wasm_bindgen]
147pub fn read_compiled_erlang(project_id: usize, module_name: &str) -> Option<String> {
148 let fs = get_filesystem(project_id);
149 let path = format!(
150 "/build/_gleam_artefacts/{}.erl",
151 module_name.replace('/', "@")
152 );
153 fs.read(&Utf8PathBuf::from(path)).ok()
154}
155
156/// Clear any stored warnings. This is performed automatically when before compilation.
157///
158#[wasm_bindgen]
159pub fn reset_warnings(project_id: usize) {
160 get_warnings(project_id).reset();
161}
162
163/// Pop the latest warning from the compiler.
164///
165#[wasm_bindgen]
166pub fn pop_warning(project_id: usize) -> Option<String> {
167 get_warnings(project_id).pop().map(|w| w.to_pretty_string())
168}
169
170fn do_compile_package(project: Project, target: Target) -> Result<(), Error> {
171 let ids = UniqueIdGenerator::new();
172 let mut type_manifests = im::HashMap::new();
173 let mut defined_modules = im::HashMap::new();
174 #[allow(clippy::arc_with_non_send_sync)]
175 let warning_emitter = WarningEmitter::new(Rc::new(project.warnings));
176 let config = PackageConfig {
177 name: "library".into(),
178 version: Version::new(1, 0, 0),
179 target,
180 ..PackageConfig::default()
181 };
182
183 let target = match target {
184 Target::Erlang => TargetCodegenConfiguration::Erlang { app_file: None },
185 Target::JavaScript => TargetCodegenConfiguration::JavaScript {
186 emit_typescript_definitions: false,
187 emit_source_maps: false,
188 prelude_location: Utf8PathBuf::from("./gleam_prelude.mjs"),
189 },
190 Target::Cranelift => TargetCodegenConfiguration::Cranelift {},
191 };
192
193 tracing::info!("Compiling package");
194
195 let lib = Utf8PathBuf::from("/lib");
196 let out = Utf8PathBuf::from("/build");
197 let package = Utf8PathBuf::from("/");
198 let mut compiler = PackageCompiler::new(
199 &config,
200 Mode::Dev,
201 &package,
202 &out,
203 &lib,
204 &target,
205 ids,
206 project.fs,
207 );
208 compiler.write_entrypoint = false;
209 compiler.write_metadata = false;
210 compiler.compile_beam_bytecode = true;
211 compiler.target_support = TargetSupport::Enforced;
212 compiler
213 .compile(
214 &warning_emitter,
215 &mut type_manifests,
216 &mut defined_modules,
217 &mut StaleTracker::default(),
218 &mut HashSet::new(),
219 &NullTelemetry,
220 )
221 .into_result()
222 .map(|_| ())
223}