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