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