Fork of daniellemaywood.uk/gleam — Wasm codegen work
38 kB
1174 lines
1mod decision;
2mod expression;
3mod import;
4#[cfg(test)]
5mod tests;
6mod typescript;
7
8use std::collections::HashMap;
9
10use num_bigint::BigInt;
11use num_traits::ToPrimitive;
12
13use crate::build::Target;
14use crate::build::package_compiler::StdlibPackage;
15use crate::codegen::TypeScriptDeclarations;
16use crate::type_::{PRELUDE_MODULE_NAME, RecordAccessor};
17use crate::{
18 ast::{CustomType, Function, Import, ModuleConstant, TypeAlias, *},
19 docvec,
20 line_numbers::LineNumbers,
21 pretty::*,
22};
23use camino::Utf8Path;
24use ecow::{EcoString, eco_format};
25use expression::Context;
26use itertools::Itertools;
27
28use self::import::{Imports, Member};
29
30const INDENT: isize = 2;
31
32pub const PRELUDE: &str = include_str!("../templates/prelude.mjs");
33pub const PRELUDE_TS_DEF: &str = include_str!("../templates/prelude.d.mts");
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum JavaScriptCodegenTarget {
37 JavaScript,
38 TypeScriptDeclarations,
39}
40
41#[derive(Debug)]
42pub struct Generator<'a> {
43 line_numbers: &'a LineNumbers,
44 module: &'a TypedModule,
45 tracker: UsageTracker,
46 module_scope: im::HashMap<EcoString, usize>,
47 current_module_name_segments_count: usize,
48 typescript: TypeScriptDeclarations,
49 stdlib_package: StdlibPackage,
50 /// Relative path to the module, surrounded in `"`s to make it a string, and with `\`s escaped
51 /// to `\\`.
52 src_path: EcoString,
53}
54
55impl<'a> Generator<'a> {
56 pub fn new(config: ModuleConfig<'a>) -> Self {
57 let ModuleConfig {
58 typescript,
59 stdlib_package,
60 module,
61 line_numbers,
62 src: _,
63 path: _,
64 project_root,
65 } = config;
66 let current_module_name_segments_count = module.name.split('/').count();
67
68 let src_path = &module.type_info.src_path;
69 let src_path = src_path
70 .strip_prefix(project_root)
71 .unwrap_or(src_path)
72 .as_str();
73 let src_path = eco_format!("\"{src_path}\"").replace("\\", "\\\\");
74
75 Self {
76 current_module_name_segments_count,
77 line_numbers,
78 module,
79 src_path,
80 tracker: UsageTracker::default(),
81 module_scope: Default::default(),
82 typescript,
83 stdlib_package,
84 }
85 }
86
87 fn type_reference(&self) -> Document<'a> {
88 if self.typescript == TypeScriptDeclarations::None {
89 return nil();
90 }
91
92 // Get the name of the module relative the directory (similar to basename)
93 let module = self
94 .module
95 .name
96 .as_str()
97 .split('/')
98 .next_back()
99 .expect("JavaScript generator could not identify imported module name.");
100
101 docvec!["/// <reference types=\"./", module, ".d.mts\" />", line()]
102 }
103
104 pub fn compile(&mut self) -> Document<'a> {
105 // Determine what JavaScript imports we need to generate
106 let mut imports = self.collect_imports();
107
108 // Determine what names are defined in the module scope so we know to
109 // rename any variables that are defined within functions using the same
110 // names.
111 self.register_module_definitions_in_scope();
112
113 // Generate JavaScript code for each statement
114 let statements = self.collect_definitions().into_iter().chain(
115 self.module
116 .definitions
117 .iter()
118 .flat_map(|definition| self.definition(definition)),
119 );
120
121 // Two lines between each statement
122 let mut statements = Itertools::intersperse(statements, lines(2)).collect_vec();
123
124 // Import any prelude functions that have been used
125
126 if self.tracker.ok_used {
127 self.register_prelude_usage(&mut imports, "Ok", None);
128 };
129
130 if self.tracker.error_used {
131 self.register_prelude_usage(&mut imports, "Error", None);
132 };
133
134 if self.tracker.list_used {
135 self.register_prelude_usage(&mut imports, "toList", None);
136 };
137
138 if self.tracker.list_empty_class_used || self.tracker.echo_used {
139 self.register_prelude_usage(&mut imports, "Empty", Some("$Empty"));
140 };
141
142 if self.tracker.list_non_empty_class_used || self.tracker.echo_used {
143 self.register_prelude_usage(&mut imports, "NonEmpty", Some("$NonEmpty"));
144 };
145
146 if self.tracker.prepend_used {
147 self.register_prelude_usage(&mut imports, "prepend", Some("listPrepend"));
148 };
149
150 if self.tracker.custom_type_used || self.tracker.echo_used {
151 self.register_prelude_usage(&mut imports, "CustomType", Some("$CustomType"));
152 };
153
154 if self.tracker.make_error_used {
155 self.register_prelude_usage(&mut imports, "makeError", None);
156 };
157
158 if self.tracker.int_remainder_used {
159 self.register_prelude_usage(&mut imports, "remainderInt", None);
160 };
161
162 if self.tracker.float_division_used {
163 self.register_prelude_usage(&mut imports, "divideFloat", None);
164 };
165
166 if self.tracker.int_division_used {
167 self.register_prelude_usage(&mut imports, "divideInt", None);
168 };
169
170 if self.tracker.object_equality_used {
171 self.register_prelude_usage(&mut imports, "isEqual", None);
172 };
173
174 if self.tracker.bit_array_literal_used {
175 self.register_prelude_usage(&mut imports, "toBitArray", None);
176 }
177
178 if self.tracker.bit_array_slice_used || self.tracker.echo_used {
179 self.register_prelude_usage(&mut imports, "bitArraySlice", None);
180 }
181
182 if self.tracker.bit_array_slice_to_float_used {
183 self.register_prelude_usage(&mut imports, "bitArraySliceToFloat", None);
184 }
185
186 if self.tracker.bit_array_slice_to_int_used || self.tracker.echo_used {
187 self.register_prelude_usage(&mut imports, "bitArraySliceToInt", None);
188 }
189
190 if self.tracker.sized_integer_segment_used {
191 self.register_prelude_usage(&mut imports, "sizedInt", None);
192 }
193
194 if self.tracker.string_bit_array_segment_used {
195 self.register_prelude_usage(&mut imports, "stringBits", None);
196 }
197
198 if self.tracker.string_utf16_bit_array_segment_used {
199 self.register_prelude_usage(&mut imports, "stringToUtf16", None);
200 }
201
202 if self.tracker.string_utf32_bit_array_segment_used {
203 self.register_prelude_usage(&mut imports, "stringToUtf32", None);
204 }
205
206 if self.tracker.codepoint_bit_array_segment_used {
207 self.register_prelude_usage(&mut imports, "codepointBits", None);
208 }
209
210 if self.tracker.codepoint_utf16_bit_array_segment_used {
211 self.register_prelude_usage(&mut imports, "codepointToUtf16", None);
212 }
213
214 if self.tracker.codepoint_utf32_bit_array_segment_used {
215 self.register_prelude_usage(&mut imports, "codepointToUtf32", None);
216 }
217
218 if self.tracker.float_bit_array_segment_used {
219 self.register_prelude_usage(&mut imports, "sizedFloat", None);
220 }
221
222 let echo_definition = self.echo_definition(&mut imports);
223 let type_reference = self.type_reference();
224 let filepath_definition = self.filepath_definition();
225
226 // Put it all together
227
228 if imports.is_empty() && statements.is_empty() {
229 docvec![
230 type_reference,
231 filepath_definition,
232 "export {}",
233 line(),
234 echo_definition
235 ]
236 } else if imports.is_empty() {
237 statements.push(line());
238 docvec![
239 type_reference,
240 filepath_definition,
241 statements,
242 echo_definition
243 ]
244 } else if statements.is_empty() {
245 docvec![
246 type_reference,
247 imports.into_doc(JavaScriptCodegenTarget::JavaScript),
248 filepath_definition,
249 echo_definition,
250 ]
251 } else {
252 docvec![
253 type_reference,
254 imports.into_doc(JavaScriptCodegenTarget::JavaScript),
255 line(),
256 filepath_definition,
257 statements,
258 line(),
259 echo_definition
260 ]
261 }
262 }
263
264 fn echo_definition(&mut self, imports: &mut Imports<'a>) -> Document<'a> {
265 if !self.tracker.echo_used {
266 return nil();
267 }
268
269 if StdlibPackage::Present == self.stdlib_package {
270 let value = Some((
271 AssignName::Variable("stdlib$dict".into()),
272 SrcSpan::default(),
273 ));
274 self.register_import(imports, "gleam_stdlib", "dict", &value, &[]);
275 }
276 self.register_prelude_usage(imports, "BitArray", Some("$BitArray"));
277 self.register_prelude_usage(imports, "List", Some("$List"));
278 self.register_prelude_usage(imports, "UtfCodepoint", Some("$UtfCodepoint"));
279 docvec![line(), std::include_str!("../templates/echo.mjs"), line()]
280 }
281
282 fn register_prelude_usage(
283 &self,
284 imports: &mut Imports<'a>,
285 name: &'static str,
286 alias: Option<&'static str>,
287 ) {
288 let path = self.import_path(&self.module.type_info.package, PRELUDE_MODULE_NAME);
289 let member = Member {
290 name: name.to_doc(),
291 alias: alias.map(|a| a.to_doc()),
292 };
293 imports.register_module(path, [], [member]);
294 }
295
296 pub fn definition(&mut self, definition: &'a TypedDefinition) -> Option<Document<'a>> {
297 match definition {
298 Definition::TypeAlias(TypeAlias { .. }) => None,
299
300 // Handled in collect_imports
301 Definition::Import(Import { .. }) => None,
302
303 // Handled in collect_definitions
304 Definition::CustomType(CustomType { .. }) => None,
305
306 // If a definition is unused then we don't need to generate code for it
307 Definition::ModuleConstant(ModuleConstant { location, .. })
308 | Definition::Function(Function { location, .. })
309 if self
310 .module
311 .unused_definition_positions
312 .contains(&location.start) =>
313 {
314 None
315 }
316
317 Definition::ModuleConstant(ModuleConstant {
318 publicity,
319 name,
320 value,
321 documentation,
322 ..
323 }) => Some(self.module_constant(*publicity, name, value, documentation)),
324
325 Definition::Function(function) => {
326 // If there's an external JavaScript implementation then it will be imported,
327 // so we don't need to generate a function definition.
328 if function.external_javascript.is_some() {
329 return None;
330 }
331
332 // If the function does not support JavaScript then we don't need to generate
333 // a function definition.
334 if !function.implementations.supports(Target::JavaScript) {
335 return None;
336 }
337
338 Some(self.module_function(function))
339 }
340 }
341 }
342
343 fn custom_type_definition(
344 &mut self,
345 name: &'a str,
346 constructors: &'a [TypedRecordConstructor],
347 publicity: Publicity,
348 opaque: bool,
349 ) -> Vec<Document<'a>> {
350 // If there's no constructors then there's nothing to do here.
351 if constructors.is_empty() {
352 return vec![];
353 }
354
355 self.tracker.custom_type_used = true;
356
357 let constructor_publicity = if opaque || publicity.is_private() {
358 Publicity::Private
359 } else {
360 Publicity::Public
361 };
362
363 let mut definitions = constructors
364 .iter()
365 .map(|constructor| self.variant_definition(constructor, name, constructor_publicity))
366 .collect_vec();
367
368 // Generate getters for fields shared between variants
369 if let Some(accessors_map) = self.module.type_info.accessors.get(name)
370 && !accessors_map.shared_accessors.is_empty()
371 // Don't bother generating shared getters when there's only one variant,
372 // since the specific accessors can always be uses instead.
373 && constructors.len() != 1
374 // Only generate accessors for the API if the constructors are public
375 && constructor_publicity.is_public()
376 {
377 definitions.push(self.shared_custom_type_fields(name, &accessors_map.shared_accessors));
378 }
379
380 definitions
381 }
382
383 fn variant_definition(
384 &self,
385 constructor: &'a TypedRecordConstructor,
386 type_name: &'a str,
387 publicity: Publicity,
388 ) -> Document<'a> {
389 let class_definition = self.variant_class_definition(constructor, publicity);
390
391 // If the custom type is private or opaque, we don't need to generate API
392 // functions for it.
393 if publicity.is_private() {
394 return class_definition;
395 }
396
397 let constructor_definition = self.variant_constructor_definition(constructor, type_name);
398 let variant_check_definition = self.variant_check_definition(constructor, type_name);
399 let fields_definition = self.variant_fields_definition(constructor, type_name);
400
401 docvec![
402 class_definition,
403 line(),
404 constructor_definition,
405 line(),
406 variant_check_definition,
407 fields_definition,
408 ]
409 }
410
411 fn variant_constructor_definition(
412 &self,
413 constructor: &'a TypedRecordConstructor,
414 type_name: &'a str,
415 ) -> Document<'a> {
416 let mut arguments = Vec::new();
417
418 for (index, parameter) in constructor.arguments.iter().enumerate() {
419 if let Some((_, label)) = ¶meter.label {
420 arguments.push(maybe_escape_identifier(label).to_doc());
421 } else {
422 arguments.push(eco_format!("${index}").to_doc());
423 }
424 }
425
426 let construction = docvec![
427 break_("", " "),
428 "new ",
429 constructor.name.as_str(),
430 "(",
431 join(arguments.clone(), break_(",", ", ")).group(),
432 ");"
433 ]
434 .group();
435
436 docvec![
437 "export const ",
438 type_name,
439 "$",
440 constructor.name.as_str(),
441 " = (",
442 join(arguments, break_(",", ", ")),
443 ") =>",
444 construction.nest(INDENT),
445 ]
446 }
447
448 fn variant_check_definition(
449 &self,
450 constructor: &'a TypedRecordConstructor,
451 type_name: &'a str,
452 ) -> Document<'a> {
453 let construction = docvec![
454 break_("", " "),
455 "value instanceof ",
456 constructor.name.as_str(),
457 ";"
458 ]
459 .group();
460
461 docvec![
462 "export const ",
463 type_name,
464 "$is",
465 constructor.name.as_str(),
466 " = (value) =>",
467 construction.nest(INDENT),
468 ]
469 }
470
471 fn variant_fields_definition(
472 &self,
473 constructor: &'a TypedRecordConstructor,
474 type_name: &'a str,
475 ) -> Document<'a> {
476 let mut functions = Vec::new();
477
478 for (index, argument) in constructor.arguments.iter().enumerate() {
479 // Always generate the accessor for the value at this index. Although
480 // this is not necessary when a label is present, we want to make sure
481 // that adding a label to a record isn't a breaking change. For this
482 // reason, we need to generate an index getter even when a label is
483 // present to ensure consistent behaviour between labelled and unlabelled
484 // field access.
485 let function_name = eco_format!(
486 "{type_name}${record_name}${index}",
487 record_name = constructor.name,
488 );
489
490 let contents;
491
492 // If the argument is labelled, also generate a getter for the labelled
493 // argument.
494 if let Some((_, label)) = &argument.label {
495 let function_name = eco_format!(
496 "{type_name}${record_name}${label}",
497 record_name = constructor.name,
498 );
499
500 contents =
501 docvec![break_("", " "), "value.", maybe_escape_property(label), ";"].group();
502
503 functions.push(docvec![
504 line(),
505 "export const ",
506 function_name,
507 " = (value) =>",
508 contents.clone().nest(INDENT),
509 ]);
510 } else {
511 contents = docvec![break_("", " "), "value[", index, "];"].group()
512 }
513
514 functions.push(docvec![
515 line(),
516 "export const ",
517 function_name,
518 " = (value) =>",
519 contents.nest(INDENT),
520 ]);
521 }
522
523 concat(functions)
524 }
525
526 fn shared_custom_type_fields(
527 &self,
528 type_name: &'a str,
529 shared_accessors: &HashMap<EcoString, RecordAccessor>,
530 ) -> Document<'a> {
531 let accessors = shared_accessors.keys().sorted().map(|field| {
532 let function_name = eco_format!("{type_name}${field}");
533
534 let contents =
535 docvec![break_("", " "), "value.", maybe_escape_property(field), ";"].group();
536
537 docvec![
538 "export const ",
539 function_name,
540 " = (value) =>",
541 contents.nest(INDENT),
542 ]
543 });
544 concat(Itertools::intersperse(accessors, line()))
545 }
546
547 fn variant_class_definition(
548 &self,
549 constructor: &'a TypedRecordConstructor,
550 publicity: Publicity,
551 ) -> Document<'a> {
552 fn parameter((i, arg): (usize, &TypedRecordConstructorArg)) -> Document<'_> {
553 arg.label
554 .as_ref()
555 .map(|(_, s)| maybe_escape_identifier(s))
556 .unwrap_or_else(|| eco_format!("${i}"))
557 .to_doc()
558 }
559
560 let doc = if let Some((_, documentation)) = &constructor.documentation {
561 jsdoc_comment(documentation, publicity).append(line())
562 } else {
563 nil()
564 };
565
566 let head = if publicity.is_public() {
567 "export class "
568 } else {
569 "class "
570 };
571 let head = docvec![head, &constructor.name, " extends $CustomType {"];
572
573 if constructor.arguments.is_empty() {
574 return head.append("}");
575 };
576
577 let parameters = join(
578 constructor.arguments.iter().enumerate().map(parameter),
579 break_(",", ", "),
580 );
581
582 let constructor_body = join(
583 constructor.arguments.iter().enumerate().map(|(i, arg)| {
584 let var = parameter((i, arg));
585 match &arg.label {
586 None => docvec!["this[", i, "] = ", var, ";"],
587 Some((_, name)) => {
588 docvec!["this.", maybe_escape_property(name), " = ", var, ";"]
589 }
590 }
591 }),
592 line(),
593 );
594
595 let class_body = docvec![
596 line(),
597 "constructor(",
598 parameters,
599 ") {",
600 docvec![line(), "super();", line(), constructor_body].nest(INDENT),
601 line(),
602 "}",
603 ]
604 .nest(INDENT);
605
606 docvec![doc, head, class_body, line(), "}"]
607 }
608
609 fn collect_definitions(&mut self) -> Vec<Document<'a>> {
610 self.module
611 .definitions
612 .iter()
613 .flat_map(|definition| match definition {
614 // If a custom type is unused then we don't need to generate code for it
615 Definition::CustomType(CustomType { location, .. })
616 if self
617 .module
618 .unused_definition_positions
619 .contains(&location.start) =>
620 {
621 vec![]
622 }
623
624 Definition::CustomType(CustomType {
625 publicity,
626 constructors,
627 opaque,
628 name,
629 ..
630 }) => self.custom_type_definition(name, constructors, *publicity, *opaque),
631
632 Definition::Function(Function { .. })
633 | Definition::TypeAlias(TypeAlias { .. })
634 | Definition::Import(Import { .. })
635 | Definition::ModuleConstant(ModuleConstant { .. }) => vec![],
636 })
637 .collect()
638 }
639
640 fn collect_imports(&mut self) -> Imports<'a> {
641 let mut imports = Imports::new();
642
643 for definition in &self.module.definitions {
644 match definition {
645 Definition::Import(Import {
646 module,
647 as_name,
648 unqualified_values: unqualified,
649 package,
650 ..
651 }) => {
652 self.register_import(&mut imports, package, module, as_name, unqualified);
653 }
654
655 Definition::Function(Function {
656 name: Some((_, name)),
657 publicity,
658 external_javascript: Some((module, function, _location)),
659 ..
660 }) => {
661 self.register_external_function(
662 &mut imports,
663 *publicity,
664 name,
665 module,
666 function,
667 );
668 }
669
670 Definition::Function(Function { .. })
671 | Definition::TypeAlias(TypeAlias { .. })
672 | Definition::CustomType(CustomType { .. })
673 | Definition::ModuleConstant(ModuleConstant { .. }) => (),
674 }
675 }
676
677 imports
678 }
679
680 fn import_path(&self, package: &'a str, module: &'a str) -> EcoString {
681 // TODO: strip shared prefixed between current module and imported
682 // module to avoid descending and climbing back out again
683 if package == self.module.type_info.package || package.is_empty() {
684 // Same package
685 match self.current_module_name_segments_count {
686 1 => eco_format!("./{module}.mjs"),
687 _ => {
688 let prefix = "../".repeat(self.current_module_name_segments_count - 1);
689 eco_format!("{prefix}{module}.mjs")
690 }
691 }
692 } else {
693 // Different package
694 let prefix = "../".repeat(self.current_module_name_segments_count);
695 eco_format!("{prefix}{package}/{module}.mjs")
696 }
697 }
698
699 fn register_import(
700 &mut self,
701 imports: &mut Imports<'a>,
702 package: &'a str,
703 module: &'a str,
704 as_name: &Option<(AssignName, SrcSpan)>,
705 unqualified: &[UnqualifiedImport],
706 ) {
707 let get_name = |module: &'a str| {
708 module
709 .split('/')
710 .next_back()
711 .expect("JavaScript generator could not identify imported module name.")
712 };
713
714 let (discarded, module_name) = match as_name {
715 None => (false, get_name(module)),
716 Some((AssignName::Discard(_), _)) => (true, get_name(module)),
717 Some((AssignName::Variable(name), _)) => (false, name.as_str()),
718 };
719
720 let module_name = eco_format!("${module_name}");
721 let path = self.import_path(package, module);
722 let unqualified_imports = unqualified.iter().map(|i| {
723 let alias = i.as_name.as_ref().map(|n| {
724 self.register_in_scope(n);
725 maybe_escape_identifier(n).to_doc()
726 });
727 let name = maybe_escape_identifier(&i.name).to_doc();
728 Member { name, alias }
729 });
730
731 let aliases = if discarded { vec![] } else { vec![module_name] };
732 imports.register_module(path, aliases, unqualified_imports);
733 }
734
735 fn register_external_function(
736 &mut self,
737 imports: &mut Imports<'a>,
738 publicity: Publicity,
739 name: &'a str,
740 module: &'a str,
741 fun: &'a str,
742 ) {
743 let needs_escaping = !is_usable_js_identifier(name);
744 let member = Member {
745 name: fun.to_doc(),
746 alias: if name == fun && !needs_escaping {
747 None
748 } else if needs_escaping {
749 Some(escape_identifier(name).to_doc())
750 } else {
751 Some(name.to_doc())
752 },
753 };
754 if publicity.is_importable() {
755 imports.register_export(maybe_escape_identifier_string(name))
756 }
757 imports.register_module(EcoString::from(module), [], [member]);
758 }
759
760 fn module_constant(
761 &mut self,
762 publicity: Publicity,
763 name: &'a EcoString,
764 value: &'a TypedConstant,
765 documentation: &'a Option<(u32, EcoString)>,
766 ) -> Document<'a> {
767 let head = if publicity.is_private() {
768 "const "
769 } else {
770 "export const "
771 };
772
773 let mut generator = expression::Generator::new(
774 self.module.name.clone(),
775 self.src_path.clone(),
776 self.line_numbers,
777 "".into(),
778 vec![],
779 &mut self.tracker,
780 self.module_scope.clone(),
781 );
782
783 let document = generator.constant_expression(Context::Constant, value);
784
785 let jsdoc = if let Some((_, documentation)) = documentation {
786 jsdoc_comment(documentation, publicity).append(line())
787 } else {
788 nil()
789 };
790
791 docvec![
792 jsdoc,
793 head,
794 maybe_escape_identifier(name),
795 " = ",
796 document,
797 ";",
798 ]
799 }
800
801 fn register_in_scope(&mut self, name: &str) {
802 let _ = self.module_scope.insert(name.into(), 0);
803 }
804
805 fn module_function(&mut self, function: &'a TypedFunction) -> Document<'a> {
806 let (_, name) = function
807 .name
808 .as_ref()
809 .expect("A module's function must be named");
810 let argument_names = function
811 .arguments
812 .iter()
813 .map(|arg| arg.names.get_variable_name())
814 .collect();
815 let mut generator = expression::Generator::new(
816 self.module.name.clone(),
817 self.src_path.clone(),
818 self.line_numbers,
819 name.clone(),
820 argument_names,
821 &mut self.tracker,
822 self.module_scope.clone(),
823 );
824
825 let function_doc = match &function.documentation {
826 None => nil(),
827 Some((_, documentation)) => {
828 jsdoc_comment(documentation, function.publicity).append(line())
829 }
830 };
831
832 let head = if function.publicity.is_private() {
833 "function "
834 } else {
835 "export function "
836 };
837
838 let body = generator.function_body(function.body.as_slice(), function.arguments.as_slice());
839
840 docvec![
841 function_doc,
842 head,
843 maybe_escape_identifier(name.as_str()),
844 fun_arguments(function.arguments.as_slice(), generator.tail_recursion_used),
845 " {",
846 docvec![line(), body].nest(INDENT).group(),
847 line(),
848 "}",
849 ]
850 }
851
852 fn register_module_definitions_in_scope(&mut self) {
853 for definition in self.module.definitions.iter() {
854 match definition {
855 Definition::ModuleConstant(ModuleConstant { name, .. }) => {
856 self.register_in_scope(name)
857 }
858
859 Definition::Function(Function { name, .. }) => self.register_in_scope(
860 name.as_ref()
861 .map(|(_, name)| name)
862 .expect("Function in a definition must be named"),
863 ),
864
865 Definition::Import(Import {
866 unqualified_values: unqualified,
867 ..
868 }) => unqualified
869 .iter()
870 .for_each(|unq_import| self.register_in_scope(unq_import.used_name())),
871
872 Definition::TypeAlias(TypeAlias { .. })
873 | Definition::CustomType(CustomType { .. }) => (),
874 }
875 }
876 }
877
878 fn filepath_definition(&self) -> Document<'a> {
879 if !self.tracker.make_error_used {
880 return nil();
881 }
882
883 docvec!["const FILEPATH = ", self.src_path.clone(), ';', lines(2)]
884 }
885}
886
887fn jsdoc_comment(documentation: &EcoString, publicity: Publicity) -> Document<'_> {
888 let doc_lines = documentation
889 .trim_end()
890 .split('\n')
891 .map(|line| eco_format!(" *{line}", line = line.replace("*/", "*\\/")).to_doc())
892 .collect_vec();
893
894 // We start with the documentation of the function
895 let doc_body = join(doc_lines, line());
896 let mut doc = docvec!["/**", line(), doc_body, line()];
897 if !publicity.is_public() {
898 // If the function is not public we hide the documentation using
899 // the `@ignore` tag: https://jsdoc.app/tags-ignore
900 doc = docvec![doc, " * ", line(), " * @ignore", line()];
901 }
902 // And finally we close the doc comment
903 docvec![doc, " */"]
904}
905
906#[derive(Debug)]
907pub struct ModuleConfig<'a> {
908 pub module: &'a TypedModule,
909 pub line_numbers: &'a LineNumbers,
910 pub src: &'a EcoString,
911 pub typescript: TypeScriptDeclarations,
912 pub stdlib_package: StdlibPackage,
913 pub path: &'a Utf8Path,
914 pub project_root: &'a Utf8Path,
915}
916
917pub fn module(config: ModuleConfig<'_>) -> String {
918 let document = Generator::new(config).compile();
919 document.to_pretty_string(80)
920}
921
922pub fn ts_declaration(module: &TypedModule) -> String {
923 let document = typescript::TypeScriptGenerator::new(module).compile();
924 document.to_pretty_string(80)
925}
926
927fn fun_arguments(arguments: &'_ [TypedArg], tail_recursion_used: bool) -> Document<'_> {
928 let mut discards = 0;
929 wrap_arguments(
930 arguments
931 .iter()
932 .map(|argument| match argument.get_variable_name() {
933 None => {
934 let doc = if discards == 0 {
935 "_".to_doc()
936 } else {
937 eco_format!("_{discards}").to_doc()
938 };
939 discards += 1;
940 doc
941 }
942 Some(name) if tail_recursion_used => eco_format!("loop${name}").to_doc(),
943 Some(name) => maybe_escape_identifier(name).to_doc(),
944 }),
945 )
946}
947
948fn wrap_arguments<'a, I>(arguments: I) -> Document<'a>
949where
950 I: IntoIterator<Item = Document<'a>>,
951{
952 break_("", "")
953 .append(join(arguments, break_(",", ", ")))
954 .nest(INDENT)
955 .append(break_("", ""))
956 .surround("(", ")")
957 .group()
958}
959
960fn wrap_object<'a>(
961 items: impl IntoIterator<Item = (Document<'a>, Option<Document<'a>>)>,
962) -> Document<'a> {
963 let mut empty = true;
964 let fields = items.into_iter().map(|(key, value)| {
965 empty = false;
966 match value {
967 Some(value) => docvec![key, ": ", value],
968 None => key.to_doc(),
969 }
970 });
971 let fields = join(fields, break_(",", ", "));
972
973 if empty {
974 "{}".to_doc()
975 } else {
976 docvec![
977 docvec!["{", break_("", " "), fields]
978 .nest(INDENT)
979 .append(break_("", " "))
980 .group(),
981 "}"
982 ]
983 }
984}
985
986fn is_usable_js_identifier(word: &str) -> bool {
987 !matches!(
988 word,
989 // Keywords and reserved words
990 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar
991 "await"
992 | "arguments"
993 | "break"
994 | "case"
995 | "catch"
996 | "class"
997 | "const"
998 | "continue"
999 | "debugger"
1000 | "default"
1001 | "delete"
1002 | "do"
1003 | "else"
1004 | "enum"
1005 | "export"
1006 | "extends"
1007 | "eval"
1008 | "false"
1009 | "finally"
1010 | "for"
1011 | "function"
1012 | "if"
1013 | "implements"
1014 | "import"
1015 | "in"
1016 | "instanceof"
1017 | "interface"
1018 | "let"
1019 | "new"
1020 | "null"
1021 | "package"
1022 | "private"
1023 | "protected"
1024 | "public"
1025 | "return"
1026 | "static"
1027 | "super"
1028 | "switch"
1029 | "this"
1030 | "throw"
1031 | "true"
1032 | "try"
1033 | "typeof"
1034 | "var"
1035 | "void"
1036 | "while"
1037 | "with"
1038 | "yield"
1039 // `undefined` to avoid any unintentional overriding.
1040 | "undefined"
1041 // `then` to avoid a module that defines a `then` function being
1042 // used as a `thenable` in JavaScript when the module is imported
1043 // dynamically, which results in unexpected behaviour.
1044 // It is rather unfortunate that we have to do this.
1045 | "then"
1046 )
1047}
1048
1049fn is_usable_js_property(label: &str) -> bool {
1050 match label {
1051 // `then` to avoid a custom type that defines a `then` function being
1052 // used as a `thenable` in Javascript.
1053 "then"
1054 // `constructor` to avoid unintentional overriding of the constructor of
1055 // records, leading to potential runtime crashes while using `withFields`.
1056 | "constructor"
1057 // `prototype` and `__proto__` to avoid unintentionally overriding the
1058 // prototype chain.
1059 | "prototype" | "__proto__" => false,
1060 _ => true
1061 }
1062}
1063
1064fn maybe_escape_identifier_string(word: &str) -> EcoString {
1065 if is_usable_js_identifier(word) {
1066 EcoString::from(word)
1067 } else {
1068 escape_identifier(word)
1069 }
1070}
1071
1072fn escape_identifier(word: &str) -> EcoString {
1073 eco_format!("{word}$")
1074}
1075
1076fn maybe_escape_identifier(word: &str) -> EcoString {
1077 if is_usable_js_identifier(word) {
1078 EcoString::from(word)
1079 } else {
1080 escape_identifier(word)
1081 }
1082}
1083
1084fn maybe_escape_property(label: &str) -> EcoString {
1085 if is_usable_js_property(label) {
1086 EcoString::from(label)
1087 } else {
1088 escape_identifier(label)
1089 }
1090}
1091
1092#[derive(Debug, Default)]
1093pub(crate) struct UsageTracker {
1094 pub ok_used: bool,
1095 pub list_used: bool,
1096 pub list_empty_class_used: bool,
1097 pub list_non_empty_class_used: bool,
1098 pub prepend_used: bool,
1099 pub error_used: bool,
1100 pub int_remainder_used: bool,
1101 pub make_error_used: bool,
1102 pub custom_type_used: bool,
1103 pub int_division_used: bool,
1104 pub float_division_used: bool,
1105 pub object_equality_used: bool,
1106 pub bit_array_literal_used: bool,
1107 pub bit_array_slice_used: bool,
1108 pub bit_array_slice_to_float_used: bool,
1109 pub bit_array_slice_to_int_used: bool,
1110 pub sized_integer_segment_used: bool,
1111 pub string_bit_array_segment_used: bool,
1112 pub string_utf16_bit_array_segment_used: bool,
1113 pub string_utf32_bit_array_segment_used: bool,
1114 pub codepoint_bit_array_segment_used: bool,
1115 pub codepoint_utf16_bit_array_segment_used: bool,
1116 pub codepoint_utf32_bit_array_segment_used: bool,
1117 pub float_bit_array_segment_used: bool,
1118 pub echo_used: bool,
1119}
1120
1121fn bool(bool: bool) -> Document<'static> {
1122 match bool {
1123 true => "true".to_doc(),
1124 false => "false".to_doc(),
1125 }
1126}
1127
1128/// Int segments <= 48 bits wide in bit arrays are within JavaScript's safe range and are evaluated
1129/// at compile time when all inputs are known. This is done for both bit array expressions and
1130/// pattern matching.
1131///
1132/// Int segments of any size could be evaluated at compile time, but currently aren't due to the
1133/// potential for causing large generated JS for inputs such as `<<0:8192>>`.
1134///
1135pub(crate) const SAFE_INT_SEGMENT_MAX_SIZE: usize = 48;
1136
1137/// Evaluates the value of an Int segment in a bit array into its corresponding bytes. This avoids
1138/// needing to do the evaluation at runtime when all inputs are known at compile-time.
1139///
1140pub(crate) fn bit_array_segment_int_value_to_bytes(
1141 mut value: BigInt,
1142 size: BigInt,
1143 endianness: Endianness,
1144) -> Vec<u8> {
1145 // Clamp negative sizes to zero
1146 let size = size.max(BigInt::ZERO);
1147
1148 // Convert size to u32. This is safe because this function isn't called with a size greater
1149 // than `SAFE_INT_SEGMENT_MAX_SIZE`.
1150 let size = size
1151 .to_u32()
1152 .expect("bit array segment size to be a valid u32");
1153
1154 // Convert negative number to two's complement representation
1155 if value < BigInt::ZERO {
1156 let value_modulus = BigInt::from(2).pow(size);
1157 value = &value_modulus + (value % &value_modulus);
1158 }
1159
1160 // Convert value to the desired number of bytes
1161 let mut bytes = vec![0u8; size as usize / 8];
1162 for byte in bytes.iter_mut() {
1163 *byte = (&value % BigInt::from(256))
1164 .to_u8()
1165 .expect("modulo result to be a valid u32");
1166 value /= BigInt::from(256);
1167 }
1168
1169 if endianness.is_big() {
1170 bytes.reverse();
1171 }
1172
1173 bytes
1174}