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