Fork of daniellemaywood.uk/gleam — Wasm codegen work
37 kB
1162 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.record_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 record_definition(
384 &self,
385 constructor: &'a TypedRecordConstructor,
386 type_name: &'a str,
387 publicity: Publicity,
388 ) -> Document<'a> {
389 let class_definition = self.record_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.record_constructor_definition(constructor, type_name);
398 let variant_check_definition = self.record_check_definition(constructor, type_name);
399 let fields_definition = self.record_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 record_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 record_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 record_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 let function_name;
480 let field;
481
482 if let Some((_, label)) = &argument.label {
483 function_name = eco_format!(
484 "{type_name}${record_name}${label}",
485 record_name = constructor.name,
486 );
487 field = eco_format!(".{field_name}", field_name = maybe_escape_property(label));
488 } else {
489 function_name = eco_format!(
490 "{type_name}${record_name}${index}",
491 record_name = constructor.name,
492 );
493 field = eco_format!("[{index}]");
494 };
495
496 let contents = docvec![break_("", " "), "value", field, ";"].group();
497
498 functions.push(docvec![
499 line(),
500 "export const ",
501 function_name,
502 " = (value) =>",
503 contents.nest(INDENT),
504 ]);
505 }
506
507 concat(functions)
508 }
509
510 fn shared_custom_type_fields(
511 &self,
512 type_name: &'a str,
513 shared_accessors: &HashMap<EcoString, RecordAccessor>,
514 ) -> Document<'a> {
515 let mut functions = Vec::new();
516
517 for field in shared_accessors.keys().sorted() {
518 let function_name = eco_format!("{type_name}${field}");
519
520 let contents =
521 docvec![break_("", " "), "value.", maybe_escape_property(field), ";"].group();
522
523 functions.push(docvec![
524 line(),
525 "export const ",
526 function_name,
527 " = (value) =>",
528 contents.nest(INDENT),
529 ]);
530 }
531
532 concat(functions)
533 }
534
535 fn record_class_definition(
536 &self,
537 constructor: &'a TypedRecordConstructor,
538 publicity: Publicity,
539 ) -> Document<'a> {
540 fn parameter((i, arg): (usize, &TypedRecordConstructorArg)) -> Document<'_> {
541 arg.label
542 .as_ref()
543 .map(|(_, s)| maybe_escape_identifier(s))
544 .unwrap_or_else(|| eco_format!("${i}"))
545 .to_doc()
546 }
547
548 let doc = if let Some((_, documentation)) = &constructor.documentation {
549 jsdoc_comment(documentation, publicity).append(line())
550 } else {
551 nil()
552 };
553
554 let head = if publicity.is_public() {
555 "export class "
556 } else {
557 "class "
558 };
559 let head = docvec![head, &constructor.name, " extends $CustomType {"];
560
561 if constructor.arguments.is_empty() {
562 return head.append("}");
563 };
564
565 let parameters = join(
566 constructor.arguments.iter().enumerate().map(parameter),
567 break_(",", ", "),
568 );
569
570 let constructor_body = join(
571 constructor.arguments.iter().enumerate().map(|(i, arg)| {
572 let var = parameter((i, arg));
573 match &arg.label {
574 None => docvec!["this[", i, "] = ", var, ";"],
575 Some((_, name)) => {
576 docvec!["this.", maybe_escape_property(name), " = ", var, ";"]
577 }
578 }
579 }),
580 line(),
581 );
582
583 let class_body = docvec![
584 line(),
585 "constructor(",
586 parameters,
587 ") {",
588 docvec![line(), "super();", line(), constructor_body].nest(INDENT),
589 line(),
590 "}",
591 ]
592 .nest(INDENT);
593
594 docvec![doc, head, class_body, line(), "}"]
595 }
596
597 fn collect_definitions(&mut self) -> Vec<Document<'a>> {
598 self.module
599 .definitions
600 .iter()
601 .flat_map(|definition| match definition {
602 // If a custom type is unused then we don't need to generate code for it
603 Definition::CustomType(CustomType { location, .. })
604 if self
605 .module
606 .unused_definition_positions
607 .contains(&location.start) =>
608 {
609 vec![]
610 }
611
612 Definition::CustomType(CustomType {
613 publicity,
614 constructors,
615 opaque,
616 name,
617 ..
618 }) => self.custom_type_definition(name, constructors, *publicity, *opaque),
619
620 Definition::Function(Function { .. })
621 | Definition::TypeAlias(TypeAlias { .. })
622 | Definition::Import(Import { .. })
623 | Definition::ModuleConstant(ModuleConstant { .. }) => vec![],
624 })
625 .collect()
626 }
627
628 fn collect_imports(&mut self) -> Imports<'a> {
629 let mut imports = Imports::new();
630
631 for definition in &self.module.definitions {
632 match definition {
633 Definition::Import(Import {
634 module,
635 as_name,
636 unqualified_values: unqualified,
637 package,
638 ..
639 }) => {
640 self.register_import(&mut imports, package, module, as_name, unqualified);
641 }
642
643 Definition::Function(Function {
644 name: Some((_, name)),
645 publicity,
646 external_javascript: Some((module, function, _location)),
647 ..
648 }) => {
649 self.register_external_function(
650 &mut imports,
651 *publicity,
652 name,
653 module,
654 function,
655 );
656 }
657
658 Definition::Function(Function { .. })
659 | Definition::TypeAlias(TypeAlias { .. })
660 | Definition::CustomType(CustomType { .. })
661 | Definition::ModuleConstant(ModuleConstant { .. }) => (),
662 }
663 }
664
665 imports
666 }
667
668 fn import_path(&self, package: &'a str, module: &'a str) -> EcoString {
669 // TODO: strip shared prefixed between current module and imported
670 // module to avoid descending and climbing back out again
671 if package == self.module.type_info.package || package.is_empty() {
672 // Same package
673 match self.current_module_name_segments_count {
674 1 => eco_format!("./{module}.mjs"),
675 _ => {
676 let prefix = "../".repeat(self.current_module_name_segments_count - 1);
677 eco_format!("{prefix}{module}.mjs")
678 }
679 }
680 } else {
681 // Different package
682 let prefix = "../".repeat(self.current_module_name_segments_count);
683 eco_format!("{prefix}{package}/{module}.mjs")
684 }
685 }
686
687 fn register_import(
688 &mut self,
689 imports: &mut Imports<'a>,
690 package: &'a str,
691 module: &'a str,
692 as_name: &Option<(AssignName, SrcSpan)>,
693 unqualified: &[UnqualifiedImport],
694 ) {
695 let get_name = |module: &'a str| {
696 module
697 .split('/')
698 .next_back()
699 .expect("JavaScript generator could not identify imported module name.")
700 };
701
702 let (discarded, module_name) = match as_name {
703 None => (false, get_name(module)),
704 Some((AssignName::Discard(_), _)) => (true, get_name(module)),
705 Some((AssignName::Variable(name), _)) => (false, name.as_str()),
706 };
707
708 let module_name = eco_format!("${module_name}");
709 let path = self.import_path(package, module);
710 let unqualified_imports = unqualified.iter().map(|i| {
711 let alias = i.as_name.as_ref().map(|n| {
712 self.register_in_scope(n);
713 maybe_escape_identifier(n).to_doc()
714 });
715 let name = maybe_escape_identifier(&i.name).to_doc();
716 Member { name, alias }
717 });
718
719 let aliases = if discarded { vec![] } else { vec![module_name] };
720 imports.register_module(path, aliases, unqualified_imports);
721 }
722
723 fn register_external_function(
724 &mut self,
725 imports: &mut Imports<'a>,
726 publicity: Publicity,
727 name: &'a str,
728 module: &'a str,
729 fun: &'a str,
730 ) {
731 let needs_escaping = !is_usable_js_identifier(name);
732 let member = Member {
733 name: fun.to_doc(),
734 alias: if name == fun && !needs_escaping {
735 None
736 } else if needs_escaping {
737 Some(escape_identifier(name).to_doc())
738 } else {
739 Some(name.to_doc())
740 },
741 };
742 if publicity.is_importable() {
743 imports.register_export(maybe_escape_identifier_string(name))
744 }
745 imports.register_module(EcoString::from(module), [], [member]);
746 }
747
748 fn module_constant(
749 &mut self,
750 publicity: Publicity,
751 name: &'a EcoString,
752 value: &'a TypedConstant,
753 documentation: &'a Option<(u32, EcoString)>,
754 ) -> Document<'a> {
755 let head = if publicity.is_private() {
756 "const "
757 } else {
758 "export const "
759 };
760
761 let mut generator = expression::Generator::new(
762 self.module.name.clone(),
763 self.src_path.clone(),
764 self.line_numbers,
765 "".into(),
766 vec![],
767 &mut self.tracker,
768 self.module_scope.clone(),
769 );
770
771 let document = generator.constant_expression(Context::Constant, value);
772
773 let jsdoc = if let Some((_, documentation)) = documentation {
774 jsdoc_comment(documentation, publicity).append(line())
775 } else {
776 nil()
777 };
778
779 docvec![
780 jsdoc,
781 head,
782 maybe_escape_identifier(name),
783 " = ",
784 document,
785 ";",
786 ]
787 }
788
789 fn register_in_scope(&mut self, name: &str) {
790 let _ = self.module_scope.insert(name.into(), 0);
791 }
792
793 fn module_function(&mut self, function: &'a TypedFunction) -> Document<'a> {
794 let (_, name) = function
795 .name
796 .as_ref()
797 .expect("A module's function must be named");
798 let argument_names = function
799 .arguments
800 .iter()
801 .map(|arg| arg.names.get_variable_name())
802 .collect();
803 let mut generator = expression::Generator::new(
804 self.module.name.clone(),
805 self.src_path.clone(),
806 self.line_numbers,
807 name.clone(),
808 argument_names,
809 &mut self.tracker,
810 self.module_scope.clone(),
811 );
812
813 let function_doc = match &function.documentation {
814 None => nil(),
815 Some((_, documentation)) => {
816 jsdoc_comment(documentation, function.publicity).append(line())
817 }
818 };
819
820 let head = if function.publicity.is_private() {
821 "function "
822 } else {
823 "export function "
824 };
825
826 let body = generator.function_body(function.body.as_slice(), function.arguments.as_slice());
827
828 docvec![
829 function_doc,
830 head,
831 maybe_escape_identifier(name.as_str()),
832 fun_arguments(function.arguments.as_slice(), generator.tail_recursion_used),
833 " {",
834 docvec![line(), body].nest(INDENT).group(),
835 line(),
836 "}",
837 ]
838 }
839
840 fn register_module_definitions_in_scope(&mut self) {
841 for definition in self.module.definitions.iter() {
842 match definition {
843 Definition::ModuleConstant(ModuleConstant { name, .. }) => {
844 self.register_in_scope(name)
845 }
846
847 Definition::Function(Function { name, .. }) => self.register_in_scope(
848 name.as_ref()
849 .map(|(_, name)| name)
850 .expect("Function in a definition must be named"),
851 ),
852
853 Definition::Import(Import {
854 unqualified_values: unqualified,
855 ..
856 }) => unqualified
857 .iter()
858 .for_each(|unq_import| self.register_in_scope(unq_import.used_name())),
859
860 Definition::TypeAlias(TypeAlias { .. })
861 | Definition::CustomType(CustomType { .. }) => (),
862 }
863 }
864 }
865
866 fn filepath_definition(&self) -> Document<'a> {
867 if !self.tracker.make_error_used {
868 return nil();
869 }
870
871 docvec!["const FILEPATH = ", self.src_path.clone(), ';', lines(2)]
872 }
873}
874
875fn jsdoc_comment(documentation: &EcoString, publicity: Publicity) -> Document<'_> {
876 let doc_lines = documentation
877 .trim_end()
878 .split('\n')
879 .map(|line| eco_format!(" *{line}", line = line.replace("*/", "*\\/")).to_doc())
880 .collect_vec();
881
882 // We start with the documentation of the function
883 let doc_body = join(doc_lines, line());
884 let mut doc = docvec!["/**", line(), doc_body, line()];
885 if !publicity.is_public() {
886 // If the function is not public we hide the documentation using
887 // the `@ignore` tag: https://jsdoc.app/tags-ignore
888 doc = docvec![doc, " * ", line(), " * @ignore", line()];
889 }
890 // And finally we close the doc comment
891 docvec![doc, " */"]
892}
893
894#[derive(Debug)]
895pub struct ModuleConfig<'a> {
896 pub module: &'a TypedModule,
897 pub line_numbers: &'a LineNumbers,
898 pub src: &'a EcoString,
899 pub typescript: TypeScriptDeclarations,
900 pub stdlib_package: StdlibPackage,
901 pub path: &'a Utf8Path,
902 pub project_root: &'a Utf8Path,
903}
904
905pub fn module(config: ModuleConfig<'_>) -> String {
906 let document = Generator::new(config).compile();
907 document.to_pretty_string(80)
908}
909
910pub fn ts_declaration(module: &TypedModule) -> String {
911 let document = typescript::TypeScriptGenerator::new(module).compile();
912 document.to_pretty_string(80)
913}
914
915fn fun_arguments(arguments: &'_ [TypedArg], tail_recursion_used: bool) -> Document<'_> {
916 let mut discards = 0;
917 wrap_arguments(
918 arguments
919 .iter()
920 .map(|argument| match argument.get_variable_name() {
921 None => {
922 let doc = if discards == 0 {
923 "_".to_doc()
924 } else {
925 eco_format!("_{discards}").to_doc()
926 };
927 discards += 1;
928 doc
929 }
930 Some(name) if tail_recursion_used => eco_format!("loop${name}").to_doc(),
931 Some(name) => maybe_escape_identifier(name).to_doc(),
932 }),
933 )
934}
935
936fn wrap_arguments<'a, I>(arguments: I) -> Document<'a>
937where
938 I: IntoIterator<Item = Document<'a>>,
939{
940 break_("", "")
941 .append(join(arguments, break_(",", ", ")))
942 .nest(INDENT)
943 .append(break_("", ""))
944 .surround("(", ")")
945 .group()
946}
947
948fn wrap_object<'a>(
949 items: impl IntoIterator<Item = (Document<'a>, Option<Document<'a>>)>,
950) -> Document<'a> {
951 let mut empty = true;
952 let fields = items.into_iter().map(|(key, value)| {
953 empty = false;
954 match value {
955 Some(value) => docvec![key, ": ", value],
956 None => key.to_doc(),
957 }
958 });
959 let fields = join(fields, break_(",", ", "));
960
961 if empty {
962 "{}".to_doc()
963 } else {
964 docvec![
965 docvec!["{", break_("", " "), fields]
966 .nest(INDENT)
967 .append(break_("", " "))
968 .group(),
969 "}"
970 ]
971 }
972}
973
974fn is_usable_js_identifier(word: &str) -> bool {
975 !matches!(
976 word,
977 // Keywords and reserved words
978 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar
979 "await"
980 | "arguments"
981 | "break"
982 | "case"
983 | "catch"
984 | "class"
985 | "const"
986 | "continue"
987 | "debugger"
988 | "default"
989 | "delete"
990 | "do"
991 | "else"
992 | "enum"
993 | "export"
994 | "extends"
995 | "eval"
996 | "false"
997 | "finally"
998 | "for"
999 | "function"
1000 | "if"
1001 | "implements"
1002 | "import"
1003 | "in"
1004 | "instanceof"
1005 | "interface"
1006 | "let"
1007 | "new"
1008 | "null"
1009 | "package"
1010 | "private"
1011 | "protected"
1012 | "public"
1013 | "return"
1014 | "static"
1015 | "super"
1016 | "switch"
1017 | "this"
1018 | "throw"
1019 | "true"
1020 | "try"
1021 | "typeof"
1022 | "var"
1023 | "void"
1024 | "while"
1025 | "with"
1026 | "yield"
1027 // `undefined` to avoid any unintentional overriding.
1028 | "undefined"
1029 // `then` to avoid a module that defines a `then` function being
1030 // used as a `thenable` in JavaScript when the module is imported
1031 // dynamically, which results in unexpected behaviour.
1032 // It is rather unfortunate that we have to do this.
1033 | "then"
1034 )
1035}
1036
1037fn is_usable_js_property(label: &str) -> bool {
1038 match label {
1039 // `then` to avoid a custom type that defines a `then` function being
1040 // used as a `thenable` in Javascript.
1041 "then"
1042 // `constructor` to avoid unintentional overriding of the constructor of
1043 // records, leading to potential runtime crashes while using `withFields`.
1044 | "constructor"
1045 // `prototype` and `__proto__` to avoid unintentionally overriding the
1046 // prototype chain.
1047 | "prototype" | "__proto__" => false,
1048 _ => true
1049 }
1050}
1051
1052fn maybe_escape_identifier_string(word: &str) -> EcoString {
1053 if is_usable_js_identifier(word) {
1054 EcoString::from(word)
1055 } else {
1056 escape_identifier(word)
1057 }
1058}
1059
1060fn escape_identifier(word: &str) -> EcoString {
1061 eco_format!("{word}$")
1062}
1063
1064fn maybe_escape_identifier(word: &str) -> EcoString {
1065 if is_usable_js_identifier(word) {
1066 EcoString::from(word)
1067 } else {
1068 escape_identifier(word)
1069 }
1070}
1071
1072fn maybe_escape_property(label: &str) -> EcoString {
1073 if is_usable_js_property(label) {
1074 EcoString::from(label)
1075 } else {
1076 escape_identifier(label)
1077 }
1078}
1079
1080#[derive(Debug, Default)]
1081pub(crate) struct UsageTracker {
1082 pub ok_used: bool,
1083 pub list_used: bool,
1084 pub list_empty_class_used: bool,
1085 pub list_non_empty_class_used: bool,
1086 pub prepend_used: bool,
1087 pub error_used: bool,
1088 pub int_remainder_used: bool,
1089 pub make_error_used: bool,
1090 pub custom_type_used: bool,
1091 pub int_division_used: bool,
1092 pub float_division_used: bool,
1093 pub object_equality_used: bool,
1094 pub bit_array_literal_used: bool,
1095 pub bit_array_slice_used: bool,
1096 pub bit_array_slice_to_float_used: bool,
1097 pub bit_array_slice_to_int_used: bool,
1098 pub sized_integer_segment_used: bool,
1099 pub string_bit_array_segment_used: bool,
1100 pub string_utf16_bit_array_segment_used: bool,
1101 pub string_utf32_bit_array_segment_used: bool,
1102 pub codepoint_bit_array_segment_used: bool,
1103 pub codepoint_utf16_bit_array_segment_used: bool,
1104 pub codepoint_utf32_bit_array_segment_used: bool,
1105 pub float_bit_array_segment_used: bool,
1106 pub echo_used: bool,
1107}
1108
1109fn bool(bool: bool) -> Document<'static> {
1110 match bool {
1111 true => "true".to_doc(),
1112 false => "false".to_doc(),
1113 }
1114}
1115
1116/// Int segments <= 48 bits wide in bit arrays are within JavaScript's safe range and are evaluated
1117/// at compile time when all inputs are known. This is done for both bit array expressions and
1118/// pattern matching.
1119///
1120/// Int segments of any size could be evaluated at compile time, but currently aren't due to the
1121/// potential for causing large generated JS for inputs such as `<<0:8192>>`.
1122///
1123pub(crate) const SAFE_INT_SEGMENT_MAX_SIZE: usize = 48;
1124
1125/// Evaluates the value of an Int segment in a bit array into its corresponding bytes. This avoids
1126/// needing to do the evaluation at runtime when all inputs are known at compile-time.
1127///
1128pub(crate) fn bit_array_segment_int_value_to_bytes(
1129 mut value: BigInt,
1130 size: BigInt,
1131 endianness: Endianness,
1132) -> Vec<u8> {
1133 // Clamp negative sizes to zero
1134 let size = size.max(BigInt::ZERO);
1135
1136 // Convert size to u32. This is safe because this function isn't called with a size greater
1137 // than `SAFE_INT_SEGMENT_MAX_SIZE`.
1138 let size = size
1139 .to_u32()
1140 .expect("bit array segment size to be a valid u32");
1141
1142 // Convert negative number to two's complement representation
1143 if value < BigInt::ZERO {
1144 let value_modulus = BigInt::from(2).pow(size);
1145 value = &value_modulus + (value % &value_modulus);
1146 }
1147
1148 // Convert value to the desired number of bytes
1149 let mut bytes = vec![0u8; size as usize / 8];
1150 for byte in bytes.iter_mut() {
1151 *byte = (&value % BigInt::from(256))
1152 .to_u8()
1153 .expect("modulo result to be a valid u32");
1154 value /= BigInt::from(256);
1155 }
1156
1157 if endianness.is_big() {
1158 bytes.reverse();
1159 }
1160
1161 bytes
1162}