1 mod expression;
2 mod import;
3 mod pattern;
4 #[cfg(test)]
5 mod tests;
6
7 use std::path::Path;
8
9 use crate::{ast::*, docvec, io::Utf8Writer, line_numbers::LineNumbers, pretty::*};
10 use itertools::Itertools;
11
12 use self::import::{Imports, Member};
13
14 const INDENT: isize = 2;
15
16 pub const PRELUDE: &str = include_str!("../templates/prelude.js");
17
18 pub type Output<'a> = Result<Document<'a>, Error>;
19
20 #[derive(Debug)]
21 pub struct Generator<'a> {
22 line_numbers: &'a LineNumbers,
23 module: &'a TypedModule,
24 tracker: UsageTracker,
25 module_scope: im::HashMap<String, usize>,
26 }
27
28 impl<'a> Generator<'a> {
new(line_numbers: &'a LineNumbers, module: &'a TypedModule) -> Self29 pub fn new(line_numbers: &'a LineNumbers, module: &'a TypedModule) -> Self {
30 Self {
31 line_numbers,
32 module,
33 tracker: UsageTracker::default(),
34 module_scope: Default::default(),
35 }
36 }
37
compile(&mut self) -> Output<'a>38 pub fn compile(&mut self) -> Output<'a> {
39 let mut imports = self.collect_imports();
40 let statements = self
41 .module
42 .statements
43 .iter()
44 .flat_map(|s| self.statement(s));
45
46 // Two lines between each statement
47 let mut statements: Vec<_> =
48 Itertools::intersperse(statements, Ok(lines(2))).try_collect()?;
49
50 // Import any prelude functions that have been used
51
52 if self.tracker.ok_used {
53 self.register_prelude_usage(&mut imports, "Ok");
54 };
55
56 if self.tracker.error_used {
57 self.register_prelude_usage(&mut imports, "Error");
58 };
59
60 if self.tracker.list_used {
61 self.register_prelude_usage(&mut imports, "toList");
62 };
63
64 if self.tracker.custom_type_used {
65 self.register_prelude_usage(&mut imports, "CustomType");
66 };
67
68 if self.tracker.throw_error_used {
69 self.register_prelude_usage(&mut imports, "throwError");
70 };
71
72 if self.tracker.float_division_used {
73 self.register_prelude_usage(&mut imports, "divideFloat");
74 };
75
76 if self.tracker.int_division_used {
77 self.register_prelude_usage(&mut imports, "divideInt");
78 };
79
80 if self.tracker.object_equality_used {
81 self.register_prelude_usage(&mut imports, "isEqual");
82 };
83
84 if self.tracker.bit_string_literal_used {
85 self.register_prelude_usage(&mut imports, "toBitString");
86 };
87
88 if self.tracker.string_bit_string_segment_used {
89 self.register_prelude_usage(&mut imports, "stringBits");
90 };
91
92 if self.tracker.codepoint_bit_string_segment_used {
93 self.register_prelude_usage(&mut imports, "codepointBits");
94 };
95
96 // Put it all together
97
98 if imports.is_empty() && statements.is_empty() {
99 Ok(docvec!("export {}", line()))
100 } else if imports.is_empty() {
101 statements.push(line());
102 Ok(statements.to_doc())
103 } else if statements.is_empty() {
104 Ok(imports.into_doc())
105 } else {
106 Ok(docvec![imports.into_doc(), line(), statements, line()])
107 }
108 }
109
register_prelude_usage(&self, imports: &mut Imports<'a>, name: &'static str)110 fn register_prelude_usage(&self, imports: &mut Imports<'a>, name: &'static str) {
111 let path = self.import_path(&self.module.type_info.package, &["gleam".to_string()]);
112 let member = Member {
113 name: name.to_doc(),
114 alias: None,
115 };
116 imports.register_module(path, [], [member]);
117 }
118
statement(&mut self, statement: &'a TypedStatement) -> Vec<Output<'a>>119 pub fn statement(&mut self, statement: &'a TypedStatement) -> Vec<Output<'a>> {
120 match statement {
121 Statement::TypeAlias { .. } | Statement::ExternalType { .. } => vec![],
122
123 Statement::Import { .. } => vec![],
124
125 Statement::CustomType {
126 public,
127 constructors,
128 opaque,
129 ..
130 } => self.custom_type_definition(constructors, *public, *opaque),
131
132 Statement::ModuleConstant {
133 public,
134 name,
135 value,
136 ..
137 } => vec![self.module_constant(*public, name, value)],
138
139 Statement::Fn {
140 arguments,
141 name,
142 body,
143 public,
144 ..
145 } => vec![self.module_function(*public, name, arguments, body)],
146
147 Statement::ExternalFn {
148 public,
149 name,
150 arguments,
151 module,
152 fun,
153 ..
154 } if module.is_empty() => vec![Ok(
155 self.global_external_function(*public, name, arguments, fun)
156 )],
157
158 Statement::ExternalFn { .. } => vec![],
159 }
160 }
161
custom_type_definition( &mut self, constructors: &'a [TypedRecordConstructor], public: bool, opaque: bool, ) -> Vec<Output<'a>>162 fn custom_type_definition(
163 &mut self,
164 constructors: &'a [TypedRecordConstructor],
165 public: bool,
166 opaque: bool,
167 ) -> Vec<Output<'a>> {
168 self.tracker.custom_type_used = true;
169 constructors
170 .iter()
171 .map(|constructor| Ok(self.record_definition(constructor, public, opaque)))
172 .collect()
173 }
174
record_definition( &self, constructor: &'a TypedRecordConstructor, public: bool, opaque: bool, ) -> Document<'a>175 fn record_definition(
176 &self,
177 constructor: &'a TypedRecordConstructor,
178 public: bool,
179 opaque: bool,
180 ) -> Document<'a> {
181 fn parameter((i, arg): (usize, &TypedRecordConstructorArg)) -> Document<'_> {
182 arg.label
183 .as_ref()
184 .map(|s| maybe_escape_identifier_doc(s))
185 .unwrap_or_else(|| Document::String(format!("x{}", i)))
186 }
187
188 let head = if public && !opaque {
189 "export class "
190 } else {
191 "class "
192 };
193 let head = docvec![head, &constructor.name, " extends CustomType {"];
194
195 if constructor.arguments.is_empty() {
196 return head.append("}");
197 };
198
199 let parameters = concat(Itertools::intersperse(
200 constructor.arguments.iter().enumerate().map(parameter),
201 break_(",", ", "),
202 ));
203
204 let constructor_body = concat(Itertools::intersperse(
205 constructor.arguments.iter().enumerate().map(|(i, arg)| {
206 let var = parameter((i, arg));
207 match &arg.label {
208 None => docvec!["this[", i, "] = ", var, ";"],
209 Some(name) => docvec!["this.", name, " = ", var, ";"],
210 }
211 }),
212 line(),
213 ));
214
215 let class_body = docvec![
216 line(),
217 "constructor(",
218 parameters,
219 ") {",
220 docvec![line(), "super();", line(), constructor_body].nest(INDENT),
221 line(),
222 "}",
223 ]
224 .nest(INDENT);
225
226 docvec![head, class_body, line(), "}"]
227 }
228
collect_imports(&mut self) -> Imports<'a>229 fn collect_imports(&mut self) -> Imports<'a> {
230 let mut imports = Imports::new();
231
232 for statement in &self.module.statements {
233 match statement {
234 Statement::Fn { .. }
235 | Statement::TypeAlias { .. }
236 | Statement::CustomType { .. }
237 | Statement::ExternalType { .. }
238 | Statement::ModuleConstant { .. } => (),
239 Statement::ExternalFn { module, .. } if module.is_empty() => (),
240
241 Statement::ExternalFn {
242 public,
243 name,
244 module,
245 fun,
246 ..
247 } => self.register_external_function(&mut imports, *public, name, module, fun),
248
249 Statement::Import {
250 module,
251 as_name,
252 unqualified,
253 package,
254 ..
255 } => {
256 self.register_import(&mut imports, package, module, as_name, unqualified);
257 }
258 }
259 }
260
261 imports
262 }
263
import_path(&self, package: &'a str, module: &'a [String]) -> String264 fn import_path(&self, package: &'a str, module: &'a [String]) -> String {
265 let path = module.join("/");
266
267 if package == self.module.type_info.package || package.is_empty() {
268 // Same package uses relative paths
269 // TODO: strip shared prefixed between current module and imported
270 // module to avoid decending and climbing back out again
271 match self.module.name.len() {
272 1 => format!("./{}.js", path),
273 _ => {
274 let prefix = "../".repeat(self.module.name.len() - 1);
275 format!("{}{}.js", prefix, path)
276 }
277 }
278 } else {
279 // Different packages uses absolute imports
280 format!("gleam-packages/{}/{}.js", package, path)
281 }
282 }
283
register_import( &mut self, imports: &mut Imports<'a>, package: &'a str, module: &'a [String], as_name: &'a Option<String>, unqualified: &'a [UnqualifiedImport], )284 fn register_import(
285 &mut self,
286 imports: &mut Imports<'a>,
287 package: &'a str,
288 module: &'a [String],
289 as_name: &'a Option<String>,
290 unqualified: &'a [UnqualifiedImport],
291 ) {
292 let module_name = as_name.as_ref().unwrap_or_else(|| {
293 module
294 .last()
295 .expect("JavaScript generator could not identify imported module name.")
296 });
297 let module_name = format!("${}", module_name);
298 let path = self.import_path(package, module);
299 let unqualified_imports = unqualified
300 .iter()
301 // We do not create a JS import for types as they are not used at runtime
302 .filter(|import| import.is_value())
303 .map(|i| {
304 let alias = i.as_name.as_ref().map(|n| {
305 self.register_in_scope(n);
306 maybe_escape_identifier_doc(n)
307 });
308 let name = maybe_escape_identifier_doc(&i.name);
309 Member { name, alias }
310 });
311 imports.register_module(path, [module_name], unqualified_imports);
312 }
313
register_external_function( &mut self, imports: &mut Imports<'a>, public: bool, name: &'a str, module: &'a str, fun: &'a str, )314 fn register_external_function(
315 &mut self,
316 imports: &mut Imports<'a>,
317 public: bool,
318 name: &'a str,
319 module: &'a str,
320 fun: &'a str,
321 ) {
322 let member = Member {
323 name: fun.to_doc(),
324 alias: if name == fun {
325 None
326 } else {
327 Some(maybe_escape_identifier_doc(name))
328 },
329 };
330 if public {
331 imports.register_export(maybe_escape_identifier_string(name))
332 }
333 imports.register_module(module.to_string(), [], [member]);
334 }
335
module_constant( &mut self, public: bool, name: &'a str, value: &'a TypedConstant, ) -> Output<'a>336 fn module_constant(
337 &mut self,
338 public: bool,
339 name: &'a str,
340 value: &'a TypedConstant,
341 ) -> Output<'a> {
342 let head = if public { "export const " } else { "const " };
343 self.register_in_scope(name);
344 Ok(docvec![
345 head,
346 maybe_escape_identifier_doc(name),
347 " = ",
348 expression::constant_expression(&mut self.tracker, value)?,
349 ";",
350 ])
351 }
352
register_in_scope(&mut self, name: &str)353 fn register_in_scope(&mut self, name: &str) {
354 let _ = self.module_scope.insert(name.to_string(), 0);
355 }
356
module_function( &mut self, public: bool, name: &'a str, args: &'a [TypedArg], body: &'a TypedExpr, ) -> Output<'a>357 fn module_function(
358 &mut self,
359 public: bool,
360 name: &'a str,
361 args: &'a [TypedArg],
362 body: &'a TypedExpr,
363 ) -> Output<'a> {
364 self.register_in_scope(name);
365 let argument_names = args
366 .iter()
367 .map(|arg| arg.names.get_variable_name())
368 .collect();
369 let mut generator = expression::Generator::new(
370 &self.module.name,
371 self.line_numbers,
372 name,
373 argument_names,
374 &mut self.tracker,
375 self.module_scope.clone(),
376 );
377 let head = if public {
378 "export function "
379 } else {
380 "function "
381 };
382 let body = generator.function_body(body, args)?;
383 Ok(docvec![
384 head,
385 maybe_escape_identifier_doc(name),
386 fun_args(args, generator.tail_recursion_used),
387 " {",
388 docvec![line(), body].nest(INDENT).group(),
389 line(),
390 "}",
391 ])
392 }
393
global_external_function<T>( &mut self, public: bool, name: &'a str, arguments: &'a [ExternalFnArg<T>], fun: &'a str, ) -> Document<'a>394 fn global_external_function<T>(
395 &mut self,
396 public: bool,
397 name: &'a str,
398 arguments: &'a [ExternalFnArg<T>],
399 fun: &'a str,
400 ) -> Document<'a> {
401 let head = if public {
402 "export function "
403 } else {
404 "function "
405 };
406 let args = external_fn_args(arguments);
407 let fun = if name == fun {
408 docvec!["globalThis.", fun]
409 } else {
410 fun.to_doc()
411 };
412 let body = docvec!["return ", fun, args.clone()];
413 let body = docvec![line(), body].nest(INDENT).group();
414 docvec![head, name, args, " {", body, line(), "}"]
415 }
416 }
417
external_fn_args<T>(arguments: &[ExternalFnArg<T>]) -> Document<'_>418 fn external_fn_args<T>(arguments: &[ExternalFnArg<T>]) -> Document<'_> {
419 wrap_args(
420 arguments
421 .iter()
422 .enumerate()
423 .map(|(index, ExternalFnArg { label, .. })| {
424 label
425 .as_ref()
426 .map(|l| l.to_doc())
427 .unwrap_or_else(|| Document::String(format!("arg{}", index)))
428 }),
429 )
430 }
431
module( module: &TypedModule, line_numbers: &LineNumbers, path: &Path, src: &str, writer: &mut impl Utf8Writer, ) -> Result<(), crate::Error>432 pub fn module(
433 module: &TypedModule,
434 line_numbers: &LineNumbers,
435 path: &Path,
436 src: &str,
437 writer: &mut impl Utf8Writer,
438 ) -> Result<(), crate::Error> {
439 Generator::new(line_numbers, module)
440 .compile()
441 .map_err(|error| crate::Error::JavaScript {
442 path: path.to_path_buf(),
443 src: src.to_string(),
444 error,
445 })?
446 .pretty_print(80, writer)
447 }
448
449 #[derive(Debug, Clone, PartialEq)]
450 pub enum Error {
451 Unsupported { feature: String, location: SrcSpan },
452 }
453
fun_args(args: &'_ [TypedArg], tail_recursion_used: bool) -> Document<'_>454 fn fun_args(args: &'_ [TypedArg], tail_recursion_used: bool) -> Document<'_> {
455 let mut discards = 0;
456 wrap_args(args.iter().map(|a| match a.get_variable_name() {
457 None => {
458 let doc = if discards == 0 {
459 "_".to_doc()
460 } else {
461 Document::String(format!("_{}", discards))
462 };
463 discards += 1;
464 doc
465 }
466 Some(name) if tail_recursion_used => Document::String(format!("loop${}", name)),
467 Some(name) => maybe_escape_identifier_doc(name),
468 }))
469 }
470
wrap_args<'a, I>(args: I) -> Document<'a> where I: IntoIterator<Item = Document<'a>>,471 fn wrap_args<'a, I>(args: I) -> Document<'a>
472 where
473 I: IntoIterator<Item = Document<'a>>,
474 {
475 break_("", "")
476 .append(concat(Itertools::intersperse(
477 args.into_iter(),
478 break_(",", ", "),
479 )))
480 .nest(INDENT)
481 .append(break_("", ""))
482 .surround("(", ")")
483 .group()
484 }
485
wrap_object<'a>( items: impl IntoIterator<Item = (Document<'a>, Option<Document<'a>>)>, ) -> Document<'a>486 fn wrap_object<'a>(
487 items: impl IntoIterator<Item = (Document<'a>, Option<Document<'a>>)>,
488 ) -> Document<'a> {
489 let mut empty = true;
490 let fields = items.into_iter().map(|(key, value)| {
491 empty = false;
492 match value {
493 Some(value) => docvec![key, ": ", value],
494 None => key.to_doc(),
495 }
496 });
497 let fields = concat(Itertools::intersperse(fields, break_(",", ", ")));
498
499 if empty {
500 "{}".to_doc()
501 } else {
502 docvec![
503 docvec!["{", break_("", " "), fields]
504 .nest(INDENT)
505 .append(break_("", " "))
506 .group(),
507 "}"
508 ]
509 }
510 }
511
try_wrap_object<'a>(items: impl IntoIterator<Item = (Document<'a>, Output<'a>)>) -> Output<'a>512 fn try_wrap_object<'a>(items: impl IntoIterator<Item = (Document<'a>, Output<'a>)>) -> Output<'a> {
513 let fields = items
514 .into_iter()
515 .map(|(key, value)| Ok(docvec![key, ": ", value?]));
516 let fields: Vec<_> = Itertools::intersperse(fields, Ok(break_(",", ", "))).try_collect()?;
517
518 Ok(docvec![
519 docvec!["{", break_("", " "), fields]
520 .nest(INDENT)
521 .append(break_("", " "))
522 .group(),
523 "}"
524 ])
525 }
526
527 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar
528 // And we add `undefined` to avoid any unintentional overriding which could
529 // cause bugs.
is_valid_js_identifier(word: &str) -> bool530 fn is_valid_js_identifier(word: &str) -> bool {
531 !matches!(
532 word,
533 "await"
534 | "arguments"
535 | "break"
536 | "case"
537 | "catch"
538 | "class"
539 | "const"
540 | "continue"
541 | "debugger"
542 | "default"
543 | "delete"
544 | "do"
545 | "else"
546 | "enum"
547 | "export"
548 | "extends"
549 | "eval"
550 | "false"
551 | "finally"
552 | "for"
553 | "function"
554 | "if"
555 | "implements"
556 | "import"
557 | "in"
558 | "instanceof"
559 | "interface"
560 | "let"
561 | "new"
562 | "null"
563 | "package"
564 | "private"
565 | "protected"
566 | "public"
567 | "return"
568 | "static"
569 | "super"
570 | "switch"
571 | "this"
572 | "throw"
573 | "true"
574 | "try"
575 | "typeof"
576 | "undefined"
577 | "var"
578 | "void"
579 | "while"
580 | "with"
581 | "yield"
582 )
583 }
584
maybe_escape_identifier_string(word: &str) -> String585 fn maybe_escape_identifier_string(word: &str) -> String {
586 if is_valid_js_identifier(word) {
587 word.to_string()
588 } else {
589 escape_identifier(word)
590 }
591 }
592
escape_identifier(word: &str) -> String593 fn escape_identifier(word: &str) -> String {
594 format!("{}$", word)
595 }
596
maybe_escape_identifier_doc(word: &str) -> Document<'_>597 fn maybe_escape_identifier_doc(word: &str) -> Document<'_> {
598 if is_valid_js_identifier(word) {
599 word.to_doc()
600 } else {
601 Document::String(escape_identifier(word))
602 }
603 }
604
605 #[derive(Debug, Default)]
606 pub(crate) struct UsageTracker {
607 pub ok_used: bool,
608 pub list_used: bool,
609 pub error_used: bool,
610 pub throw_error_used: bool,
611 pub custom_type_used: bool,
612 pub int_division_used: bool,
613 pub float_division_used: bool,
614 pub object_equality_used: bool,
615 pub bit_string_literal_used: bool,
616 pub string_bit_string_segment_used: bool,
617 pub codepoint_bit_string_segment_used: bool,
618 }
619