Fork of daniellemaywood.uk/gleam — Wasm codegen work
14 kB
471 lines
1use crate::{
2 ast::{SrcSpan, TypedModule, UntypedModule},
3 build::{dep_tree, Module, Origin, Package, Target},
4 codegen::{Erlang, ErlangApp, JavaScript},
5 config::PackageConfig,
6 error,
7 io::{CommandExecutor, FileSystemIO, FileSystemReader, FileSystemWriter},
8 metadata::ModuleEncoder,
9 parse::extra::ModuleExtra,
10 type_, Error, Result, Warning,
11};
12use askama::Template;
13use std::{collections::HashMap, fmt::write};
14use std::{
15 collections::HashSet,
16 path::{Path, PathBuf},
17};
18
19#[derive(Debug)]
20pub struct PackageCompiler<'a, IO> {
21 pub io: IO,
22 pub out: &'a Path,
23 pub root: &'a Path,
24 pub target: Target,
25 pub config: &'a PackageConfig,
26 pub sources: Vec<Source>,
27 pub erl_libs: &'a str,
28 pub write_metadata: bool,
29 pub compile_erlang: bool,
30 pub write_entrypoint: bool,
31}
32
33// TODO: ensure this is not a duplicate module
34// TODO: tests
35// Including cases for:
36// - modules that don't import anything
37impl<'a, IO> PackageCompiler<'a, IO>
38where
39 IO: FileSystemIO + CommandExecutor + Clone,
40{
41 pub fn new(
42 config: &'a PackageConfig,
43 root: &'a Path,
44 out: &'a Path,
45 target: Target,
46 erl_libs: &'a str,
47 io: IO,
48 ) -> Self {
49 Self {
50 io,
51 out,
52 root,
53 config,
54 target,
55 sources: vec![],
56 erl_libs,
57 compile_erlang: true,
58 write_metadata: true,
59 write_entrypoint: false,
60 }
61 }
62
63 pub fn compile(
64 mut self,
65 warnings: &mut Vec<Warning>,
66 existing_modules: &mut HashMap<String, type_::Module>,
67 already_defined_modules: &mut HashMap<String, PathBuf>,
68 ) -> Result<Vec<Module>, Error> {
69 let span = tracing::info_span!("compile", package = %self.config.name.as_str());
70 let _enter = span.enter();
71
72 tracing::info!("Parsing source code");
73 let parsed_modules = parse_sources(
74 &self.config.name,
75 std::mem::take(&mut self.sources),
76 already_defined_modules,
77 )?;
78
79 // Determine order in which modules are to be processed
80 let sequence = dep_tree::toposort_deps(
81 parsed_modules
82 .values()
83 .map(|m| module_deps_for_graph(self.target, m))
84 .collect(),
85 )
86 .map_err(convert_deps_tree_error)?;
87
88 tracing::info!("Type checking modules");
89 let mut modules = type_check(
90 &self.config.name,
91 self.target,
92 sequence,
93 parsed_modules,
94 existing_modules,
95 warnings,
96 )?;
97
98 tracing::info!("Performing code generation");
99 self.perform_codegen(&modules)?;
100
101 self.encode_and_write_metadata(&modules)?;
102
103 Ok(modules)
104 }
105
106 fn compile_erlang_to_beam(&self, modules: &[PathBuf]) -> Result<(), Error> {
107 tracing::info!("compiling_erlang");
108
109 let env = [
110 ("ERL_LIBS", self.erl_libs.to_string()),
111 ("TERM", "dumb".into()),
112 ];
113 let mut args = vec![
114 // Use a compile server to avoid repeatedly starting VM
115 "-server".into(),
116 // Write compiled .beam to ./ebin
117 "-o".into(),
118 self.out.join("ebin").to_string_lossy().to_string(),
119 ];
120 // Add the list of modules to compile
121 for module in modules {
122 args.push(
123 self.out
124 .join("build")
125 .join(module)
126 .to_string_lossy()
127 .to_string(),
128 );
129 }
130 let status = self.io.exec("erlc", &args, &env, None)?;
131
132 if status == 0 {
133 Ok(())
134 } else {
135 Err(Error::ShellCommand {
136 command: "erlc".to_string(),
137 err: None,
138 })
139 }
140 }
141
142 fn copy_project_erlang_files(&mut self, modules: &mut Vec<PathBuf>) -> Result<(), Error> {
143 tracing::info!("copying_erlang_source_files");
144 let src = self.root.join("src");
145 let test = self.root.join("test");
146 let mut copied = HashSet::new();
147 self.copy_erlang_files(&src, &mut copied, modules)?;
148 if self.io.is_directory(&test) {
149 self.copy_erlang_files(&test, &mut copied, modules)?;
150 }
151 Ok(())
152 }
153
154 fn copy_erlang_files(
155 &self,
156 src_path: &Path,
157 copied: &mut HashSet<PathBuf>,
158 to_compile_modules: &mut Vec<PathBuf>,
159 ) -> Result<()> {
160 let out = self.out.join("build");
161 self.io.mkdir(&out)?;
162
163 for entry in self.io.read_dir(src_path)? {
164 let path = entry.expect("copy_erlang_files dir_entry").path();
165
166 let extension = path
167 .extension()
168 .unwrap_or_default()
169 .to_str()
170 .unwrap_or_default();
171 let relative_path = path
172 .strip_prefix(src_path)
173 .expect("copy_erlang_files strip prefix")
174 .to_path_buf();
175
176 match extension {
177 "hrl" => (),
178 "erl" => {
179 to_compile_modules.push(relative_path.clone());
180 }
181 _ => continue,
182 };
183
184 self.io.copy(&path, &out.join(&relative_path))?;
185
186 // TODO: test
187 if !copied.insert(relative_path.clone()) {
188 return Err(Error::DuplicateErlangFile {
189 file: relative_path.to_string_lossy().to_string(),
190 });
191 }
192 }
193 Ok(())
194 }
195
196 fn encode_and_write_metadata(&mut self, modules: &[Module]) -> Result<()> {
197 if !self.write_metadata {
198 tracing::info!("Package metadata writing disabled");
199 return Ok(());
200 }
201 tracing::info!("Writing package metadata to disc");
202 for module in modules {
203 let name = format!("{}.gleam_module", &module.name.replace('/', "@"));
204 let path = self.out.join("build").join(name);
205 ModuleEncoder::new(&module.ast.type_info).write(self.io.writer(&path)?)?;
206 }
207 Ok(())
208 }
209
210 pub fn read_source_files(&mut self) -> Result<()> {
211 let span = tracing::info_span!("load", package = %self.config.name.as_str());
212 let _enter = span.enter();
213 tracing::info!("Reading source files");
214 let src = self.root.join("src");
215 let test = self.root.join("test");
216
217 // Src
218 for path in self.io.gleam_source_files(&src) {
219 self.add_module(path, &src, Origin::Src)?;
220 }
221
222 // Test
223 if self.io.is_directory(&test) {
224 for path in self.io.gleam_source_files(&test) {
225 self.add_module(path, &test, Origin::Test)?;
226 }
227 }
228 Ok(())
229 }
230
231 fn add_module(&mut self, path: PathBuf, dir: &Path, origin: Origin) -> Result<()> {
232 let name = module_name(&dir, &path);
233 let code = self.io.read(&path)?;
234 self.sources.push(Source {
235 name,
236 path,
237 code,
238 origin,
239 });
240 Ok(())
241 }
242
243 fn perform_codegen(&mut self, modules: &[Module]) -> Result<()> {
244 let artifact_dir = self.out.join("build");
245 match self.target {
246 Target::JavaScript => {
247 JavaScript::new(&artifact_dir).render(&self.io, modules)?;
248 }
249
250 Target::Erlang => {
251 let io = self.io.clone();
252 Erlang::new(&artifact_dir).render(io.clone(), modules)?;
253 ErlangApp::new(&self.out.join("ebin")).render(io, &self.config, modules)?;
254 let mut erlang = vec![];
255
256 if self.write_entrypoint {
257 self.render_entrypoint_module(&artifact_dir, &mut erlang)?;
258 }
259
260 if self.compile_erlang {
261 erlang.extend(modules.iter().map(Module::compiled_erlang_path));
262 self.copy_project_erlang_files(&mut erlang)?;
263 self.compile_erlang_to_beam(&erlang)?;
264 }
265 }
266 }
267 Ok(())
268 }
269
270 fn render_entrypoint_module(
271 &self,
272 out: &Path,
273 modules_to_compile: &mut Vec<PathBuf>,
274 ) -> Result<(), Error> {
275 let name = "gleam@@main.erl";
276 let module = ErlangEntrypointModule {
277 application: &self.config.name,
278 }
279 .render()
280 .expect("Erlang entrypoint rendering");
281 self.io.writer(&out.join(name))?.write(module.as_bytes())?;
282 modules_to_compile.push(name.into());
283 Ok(())
284 }
285}
286
287fn type_check(
288 package_name: &str,
289 target: Target,
290 sequence: Vec<String>,
291 mut parsed_modules: HashMap<String, Parsed>,
292 module_types: &mut HashMap<String, type_::Module>,
293 warnings: &mut Vec<Warning>,
294) -> Result<Vec<Module>, Error> {
295 let mut modules = Vec::with_capacity(parsed_modules.len() + 1);
296 let mut uid = 0;
297
298 // Insert the prelude
299 // DUPE: preludeinsertion
300 // TODO: Currently we do this here and also in the tests. It would be better
301 // to have one place where we create all this required state for use in each
302 // place.
303 let _ = module_types.insert("gleam".to_string(), type_::build_prelude(&mut uid));
304
305 for name in sequence {
306 let Parsed {
307 name,
308 code,
309 ast,
310 path,
311 origin,
312 package,
313 extra,
314 } = parsed_modules
315 .remove(&name)
316 .expect("Getting parsed module for name");
317
318 tracing::debug!(module = ?name, "Type checking");
319 let mut type_warnings = Vec::new();
320 let ast = type_::infer_module(
321 target,
322 &mut uid,
323 ast,
324 origin,
325 package_name,
326 module_types,
327 &mut type_warnings,
328 )
329 .map_err(|error| Error::Type {
330 path: path.clone(),
331 src: code.clone(),
332 error,
333 })?;
334
335 // Register any warnings emitted as type warnings
336 let type_warnings = type_warnings
337 .into_iter()
338 .map(|w| w.into_warning(path.clone(), code.clone()));
339 warnings.extend(type_warnings);
340
341 // Register the types from this module so they can be imported into
342 // other modules.
343 let _ = module_types.insert(name.clone(), ast.type_info.clone());
344
345 // Register the successfully type checked module data so that it can be
346 // used for code generation
347 modules.push(Module {
348 origin,
349 extra,
350 name,
351 code,
352 ast,
353 input_path: path,
354 });
355 }
356
357 Ok(modules)
358}
359
360fn convert_deps_tree_error(e: dep_tree::Error) -> Error {
361 match e {
362 dep_tree::Error::Cycle(modules) => Error::ImportCycle { modules },
363 }
364}
365
366fn module_deps_for_graph(target: Target, module: &Parsed) -> (String, Vec<String>) {
367 let name = module.name.clone();
368 let deps: Vec<_> = module
369 .ast
370 .dependencies(target)
371 .into_iter()
372 .map(|(dep, _span)| dep)
373 .collect();
374 (name, deps)
375}
376
377fn parse_sources(
378 package_name: &str,
379 sources: Vec<Source>,
380 already_defined_modules: &mut HashMap<String, PathBuf>,
381) -> Result<HashMap<String, Parsed>, Error> {
382 let mut parsed_modules = HashMap::with_capacity(sources.len());
383 for Source {
384 name,
385 code,
386 path,
387 origin,
388 } in sources
389 {
390 let (mut ast, extra) = crate::parse::parse_module(&code).map_err(|error| Error::Parse {
391 path: path.clone(),
392 src: code.clone(),
393 error,
394 })?;
395
396 // Store the name
397 ast.name = name.split("/").map(String::from).collect(); // TODO: store the module name as a string
398
399 let module = Parsed {
400 package: package_name.to_string(),
401 origin,
402 extra,
403 path,
404 name,
405 code,
406 ast,
407 };
408
409 // Ensure there are no modules defined that already have this name
410 if let Some(first) =
411 already_defined_modules.insert(module.name.clone(), module.path.clone())
412 {
413 return Err(Error::DuplicateModule {
414 module: module.name.clone(),
415 first,
416 second: module.path.clone(),
417 });
418 }
419
420 // Register the parsed module
421 let _ = parsed_modules.insert(module.name.clone(), module);
422 }
423 Ok(parsed_modules)
424}
425
426fn module_name(package_path: &Path, full_module_path: &Path) -> String {
427 // /path/to/project/_build/default/lib/the_package/src/my/module.gleam
428
429 // my/module.gleam
430 let mut module_path = full_module_path
431 .strip_prefix(package_path)
432 .expect("Stripping package prefix from module path")
433 .to_path_buf();
434
435 // my/module
436 let _ = module_path.set_extension("");
437
438 // Stringify
439 let name = module_path
440 .to_str()
441 .expect("Module name path to str")
442 .to_string();
443
444 // normalise windows paths
445 name.replace("\\", "/")
446}
447
448#[derive(Debug)]
449pub struct Source {
450 pub path: PathBuf,
451 pub name: String,
452 pub code: String,
453 pub origin: Origin, // TODO: is this used?
454}
455
456#[derive(Debug)]
457struct Parsed {
458 path: PathBuf,
459 name: String,
460 code: String,
461 origin: Origin,
462 package: String,
463 ast: UntypedModule,
464 extra: ModuleExtra,
465}
466
467#[derive(Template)]
468#[template(path = "gleam@@main.erl", escape = "none")]
469struct ErlangEntrypointModule<'a> {
470 application: &'a str,
471}