Fork of daniellemaywood.uk/gleam — Wasm codegen work
55 kB
1661 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2021 The Gleam contributors
3
4mod decision;
5mod expression;
6mod import;
7#[cfg(test)]
8mod tests;
9mod typescript;
10
11use std::cell::RefCell;
12use std::collections::{HashMap, HashSet};
13use std::rc::Rc;
14
15use debug_ignore::DebugIgnore;
16use num_bigint::BigInt;
17use num_traits::ToPrimitive;
18use sourcemap::SourceMap;
19
20use crate::build::Target;
21use crate::build::package_compiler::StdlibPackage;
22use crate::codegen::TypeScriptDeclarations;
23use crate::line_numbers::LineColumn;
24use crate::type_::{PRELUDE_MODULE_NAME, RecordAccessor};
25use crate::{
26 ast::{Import, *},
27 line_numbers::LineNumbers,
28};
29use camino::Utf8Path;
30use ecow::{EcoString, eco_format};
31use expression::Context;
32use itertools::Itertools;
33use pretty_arena::*;
34
35use self::import::{Imports, Member};
36
37const INDENT: isize = 2;
38
39pub const PRELUDE: &str = include_str!("../templates/prelude.mjs");
40pub const PRELUDE_TS_DEF: &str = include_str!("../templates/prelude.d.mts");
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum JavaScriptCodegenTarget {
44 JavaScript,
45 TypeScriptDeclarations,
46}
47
48/// A cursor position observer that does nothing.
49/// Mainly used to allow us to create a cursor position observer that does
50/// nothing without having to check if the source map builder is present.
51#[derive(Debug, Clone, Copy)]
52pub struct NullCursorPositionObserver;
53
54impl CursorPositionObserver for NullCursorPositionObserver {
55 fn observe_cursor_position(&mut self, _line: isize, _width: isize) {
56 // Do nothing
57 }
58}
59
60/// A cursor position observer that observes the cursor position and adds it
61/// to the source map builder. When notified it will take the destination
62/// location and map it to the initial source location given when
63/// creating the observer.
64#[derive(Debug)]
65pub struct SourceMapCursorPositionObserver {
66 source_start_location: LineColumn,
67 source_map_builder: Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>,
68}
69
70impl SourceMapCursorPositionObserver {
71 pub fn new(
72 source_start_location: LineColumn,
73 source_map_builder: Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>,
74 ) -> Self {
75 Self {
76 source_start_location,
77 source_map_builder,
78 }
79 }
80}
81
82impl CursorPositionObserver for SourceMapCursorPositionObserver {
83 fn observe_cursor_position(&mut self, line: isize, column: isize) {
84 let _ = self.source_map_builder.borrow_mut().add_raw(
85 line as u32,
86 column as u32,
87 // SourceMapBuilder expects 0-based line and column numbers and we use 1-based ones
88 self.source_start_location.line - 1,
89 self.source_start_location.column - 1,
90 // Since we have a 1-1 mapping between source and destination locations
91 // We always use 0 for the source index.
92 Some(0),
93 None,
94 false,
95 );
96 }
97}
98
99#[derive(Debug)]
100pub struct Generator<'a> {
101 line_numbers: &'a LineNumbers,
102 module: &'a TypedModule,
103 tracker: UsageTracker,
104 module_scope: im::HashMap<EcoString, usize>,
105 current_module_name_segments_count: usize,
106 typescript: TypeScriptDeclarations,
107 // Debug ignored since SourceMapBuilder doesn't implement debug
108 source_map_builder: Option<Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>>,
109 stdlib_package: StdlibPackage,
110 /// Relative path to the module, surrounded in `"`s to make it a string, and with `\`s escaped
111 /// to `\\`.
112 src_path: EcoString,
113}
114
115impl<'a, 'doc> Generator<'a> {
116 pub fn new(config: ModuleConfig<'a>) -> Self {
117 let ModuleConfig {
118 typescript,
119 source_map,
120 stdlib_package,
121 module,
122 line_numbers,
123 src: _,
124 path: _,
125 project_root,
126 } = config;
127 let current_module_name_segments_count = module.name.split('/').count();
128
129 let src_path = &module.type_info.src_path;
130 let src_path = src_path
131 .strip_prefix(project_root)
132 .unwrap_or(src_path)
133 .as_str();
134 let src_path = eco_format!("\"{src_path}\"").replace("\\", "\\\\");
135 Self {
136 current_module_name_segments_count,
137 line_numbers,
138 module,
139 src_path,
140 tracker: UsageTracker::default(),
141 module_scope: im::HashMap::new(),
142 typescript,
143 source_map_builder: if source_map {
144 let module_name = module.name.clone();
145 let output_path = format!("{module_name}.mjs");
146 let module_alias = module_name.split('/').next_back().unwrap_or(&module_name);
147 let input_path = format!("{module_alias}.gleam",);
148 let mut source_map_builder = sourcemap::SourceMapBuilder::new(Some(&output_path));
149 let _ = source_map_builder.add_source(&input_path);
150 Some(Rc::new(RefCell::new(DebugIgnore(source_map_builder))))
151 } else {
152 None
153 },
154 stdlib_package,
155 }
156 }
157
158 fn type_reference(&self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> {
159 if self.typescript == TypeScriptDeclarations::None {
160 return EMPTY_DOCUMENT;
161 }
162
163 // Get the name of the module relative the directory (similar to basename)
164 let module = self
165 .module
166 .name
167 .as_str()
168 .split('/')
169 .next_back()
170 .expect("JavaScript generator could not identify imported module name.");
171
172 docvec![
173 arena,
174 REFERENCE_TYPES_DOCUMENT,
175 module,
176 DOT_D_DOT_MTS_CLOSE_QUOTE_CLOSE_TAG_DOCUMENT,
177 LINE_DOCUMENT
178 ]
179 }
180
181 fn sourcemap_reference(&self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> {
182 match self.source_map_builder {
183 None => "".to_doc(arena),
184 Some(_) => {
185 // Get the name of the module relative the directory (similar to basename)
186 let module = self
187 .module
188 .name
189 .as_str()
190 .split('/')
191 .next_back()
192 .expect("JavaScript generator could not identify imported module name.");
193
194 docvec![
195 arena,
196 SOURCE_MAPPING_URL_EQUAL_DOCUMENT,
197 module,
198 DOT_MJS_DOT_MAP_DOCUMENT,
199 LINE_DOCUMENT
200 ]
201 }
202 }
203 }
204
205 pub fn compile(&mut self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> {
206 // Determine what JavaScript imports we need to generate
207 let mut imports = self.collect_imports(arena);
208
209 // Determine what names are defined in the module scope so we know to
210 // rename any variables that are defined within functions using the same
211 // names.
212 self.register_module_definitions_in_scope();
213
214 // Generate JavaScript code for each statement.
215 let statements = self.definitions(arena);
216
217 // Two lines between each statement
218 let no_statements = statements.is_empty();
219 let statements = arena.join(statements, TWO_LINES_DOCUMENT);
220
221 // Import any prelude functions that have been used
222
223 if self.tracker.ok_used {
224 self.register_prelude_usage(arena, &mut imports, "Ok", None);
225 };
226
227 if self.tracker.error_used {
228 self.register_prelude_usage(arena, &mut imports, "Error", None);
229 };
230
231 if self.tracker.list_used {
232 self.register_prelude_usage(arena, &mut imports, "toList", None);
233 };
234
235 if self.tracker.list_empty_class_used || self.tracker.echo_used {
236 self.register_prelude_usage(arena, &mut imports, "Empty", Some("$Empty"));
237 };
238
239 if self.tracker.list_empty_const_used {
240 self.register_prelude_usage(
241 arena,
242 &mut imports,
243 "List$Empty$const",
244 Some("$List$Empty$const"),
245 );
246 };
247
248 if self.tracker.list_non_empty_class_used || self.tracker.echo_used {
249 self.register_prelude_usage(arena, &mut imports, "NonEmpty", Some("$NonEmpty"));
250 };
251
252 if self.tracker.prepend_used {
253 self.register_prelude_usage(arena, &mut imports, "prepend", Some("listPrepend"));
254 };
255
256 if self.tracker.custom_type_used || self.tracker.echo_used {
257 self.register_prelude_usage(arena, &mut imports, "CustomType", Some("$CustomType"));
258 };
259
260 if self.tracker.make_error_used {
261 self.register_prelude_usage(arena, &mut imports, "makeError", None);
262 };
263
264 if self.tracker.int_remainder_used {
265 self.register_prelude_usage(arena, &mut imports, "remainderInt", None);
266 };
267
268 if self.tracker.float_division_used {
269 self.register_prelude_usage(arena, &mut imports, "divideFloat", None);
270 };
271
272 if self.tracker.int_division_used {
273 self.register_prelude_usage(arena, &mut imports, "divideInt", None);
274 };
275
276 if self.tracker.object_equality_used {
277 self.register_prelude_usage(arena, &mut imports, "isEqual", None);
278 };
279
280 if self.tracker.bit_array_literal_used {
281 self.register_prelude_usage(arena, &mut imports, "toBitArray", None);
282 }
283
284 if self.tracker.bit_array_slice_used || self.tracker.echo_used {
285 self.register_prelude_usage(arena, &mut imports, "bitArraySlice", None);
286 }
287
288 if self.tracker.bit_array_slice_to_float_used {
289 self.register_prelude_usage(arena, &mut imports, "bitArraySliceToFloat", None);
290 }
291
292 if self.tracker.bit_array_slice_to_int_used || self.tracker.echo_used {
293 self.register_prelude_usage(arena, &mut imports, "bitArraySliceToInt", None);
294 }
295
296 if self.tracker.sized_integer_segment_used {
297 self.register_prelude_usage(arena, &mut imports, "sizedInt", None);
298 }
299
300 if self.tracker.string_bit_array_segment_used {
301 self.register_prelude_usage(arena, &mut imports, "stringBits", None);
302 }
303
304 if self.tracker.string_utf16_bit_array_segment_used {
305 self.register_prelude_usage(arena, &mut imports, "stringToUtf16", None);
306 }
307
308 if self.tracker.string_utf32_bit_array_segment_used {
309 self.register_prelude_usage(arena, &mut imports, "stringToUtf32", None);
310 }
311
312 if self.tracker.codepoint_bit_array_segment_used {
313 self.register_prelude_usage(arena, &mut imports, "codepointBits", None);
314 }
315
316 if self.tracker.codepoint_utf16_bit_array_segment_used {
317 self.register_prelude_usage(arena, &mut imports, "codepointToUtf16", None);
318 }
319
320 if self.tracker.codepoint_utf32_bit_array_segment_used {
321 self.register_prelude_usage(arena, &mut imports, "codepointToUtf32", None);
322 }
323
324 if self.tracker.float_bit_array_segment_used {
325 self.register_prelude_usage(arena, &mut imports, "sizedFloat", None);
326 }
327
328 for (variant, alias) in self.tracker.variant_constants_used.iter() {
329 let path = self.import_path(&variant.package, &variant.module);
330 let member = Member {
331 name: eco_format!("{}${}$const", variant.type_name, variant.name),
332 alias: alias.as_ref().map(|alias| alias.to_doc(arena)),
333 };
334 imports.register_module(path, [], [member]);
335 }
336
337 // If we have some Gleam code that looks something like this:
338 // ```gleam
339 // import option.{None}
340 //
341 // pub fn main() {
342 // None
343 // }
344 // ```
345 //
346 // Here, the generated JavaScript code for `main` will look something
347 // like this:
348 //
349 // ```javascript
350 // export function main() {
351 // return Option$None$const;
352 // }
353 // ```
354 //
355 // The `None` variant is technically being imported, but we don't
356 // actually use the generated `None` class in the JavaScript code, so
357 // we don't want to import it.
358 //
359 // However, if we are pattern matching on the `None` variant, there
360 // will be some code that looks like `if (value instanceof None)`, in
361 // which case we still need to import `None`. Therefore we remove all
362 // variants which are used as singleton constants, and that are not used
363 // in pattern matching.
364 //
365 imports.filter_unused_variants(
366 self.tracker
367 .variant_constants_used
368 .keys()
369 .filter(|variant| !self.tracker.variants_used_in_instanceof.contains(variant))
370 .map(|variant| {
371 let path = self.import_path(&variant.package, &variant.module);
372 (path, variant.name.clone())
373 }),
374 );
375
376 let echo_definition = self.echo_definition(arena, &mut imports);
377 let sourcemap_reference = self.sourcemap_reference(arena);
378 let type_reference = self.type_reference(arena);
379 let filepath_definition = self.filepath_definition(arena);
380
381 // Put it all together
382
383 if imports.is_empty() && no_statements {
384 docvec![
385 arena,
386 sourcemap_reference,
387 type_reference,
388 filepath_definition,
389 EXPORT_SPACE_OPEN_CLOSE_CURLY_DOCUMENT,
390 LINE_DOCUMENT,
391 echo_definition
392 ]
393 } else if imports.is_empty() {
394 docvec![
395 arena,
396 sourcemap_reference,
397 type_reference,
398 filepath_definition,
399 statements.append(arena, LINE_DOCUMENT),
400 echo_definition
401 ]
402 } else if no_statements {
403 docvec![
404 arena,
405 sourcemap_reference,
406 type_reference,
407 imports.into_doc(arena, JavaScriptCodegenTarget::JavaScript),
408 filepath_definition,
409 echo_definition,
410 ]
411 } else {
412 docvec![
413 arena,
414 sourcemap_reference,
415 type_reference,
416 imports.into_doc(arena, JavaScriptCodegenTarget::JavaScript),
417 LINE_DOCUMENT,
418 filepath_definition,
419 statements,
420 LINE_DOCUMENT,
421 echo_definition
422 ]
423 }
424 }
425
426 fn echo_definition(
427 &mut self,
428 arena: &'doc DocumentArena<'a, 'doc>,
429 imports: &mut Imports<'a, 'doc>,
430 ) -> Document<'a, 'doc> {
431 if !self.tracker.echo_used {
432 return EMPTY_DOCUMENT;
433 }
434
435 if StdlibPackage::Present == self.stdlib_package {
436 let value = Some((
437 AssignName::Variable("stdlib$dict".into()),
438 SrcSpan::default(),
439 ));
440
441 self.register_import(arena, imports, "gleam_stdlib", "gleam/dict", &value, &[]);
442 }
443 self.register_prelude_usage(arena, imports, "BitArray", Some("$BitArray"));
444 self.register_prelude_usage(arena, imports, "List", Some("$List"));
445 self.register_prelude_usage(arena, imports, "UtfCodepoint", Some("$UtfCodepoint"));
446 docvec![
447 arena,
448 LINE_DOCUMENT,
449 std::include_str!("../templates/echo.mjs"),
450 LINE_DOCUMENT
451 ]
452 }
453
454 fn register_prelude_usage(
455 &self,
456 arena: &'doc DocumentArena<'a, 'doc>,
457 imports: &mut Imports<'a, 'doc>,
458 name: &'static str,
459 alias: Option<&'static str>,
460 ) {
461 let path = self.import_path(&self.module.type_info.package, PRELUDE_MODULE_NAME);
462 let member = Member {
463 name: name.into(),
464 alias: alias.map(|a| a.to_doc(arena)),
465 };
466 imports.register_module(path, [], [member]);
467 }
468
469 fn custom_type_definition(
470 &mut self,
471 arena: &'doc DocumentArena<'a, 'doc>,
472 custom_type: &'a TypedCustomType,
473 ) -> Option<Vec<Document<'a, 'doc>>> {
474 if self
475 .module
476 .unused_definition_positions
477 .contains(&custom_type.location.start)
478 {
479 return None;
480 }
481
482 let TypedCustomType {
483 name,
484 publicity,
485 constructors,
486 opaque,
487 ..
488 } = custom_type;
489
490 // If there's no constructors then there's nothing to do here.
491 if constructors.is_empty() {
492 return Some(vec![]);
493 }
494
495 self.tracker.custom_type_used = true;
496
497 let constructor_publicity = if *opaque || publicity.is_private() {
498 Publicity::Private
499 } else {
500 Publicity::Public
501 };
502
503 let mut definitions = constructors
504 .iter()
505 .map(|constructor| {
506 self.variant_definition(arena, constructor, name, constructor_publicity)
507 })
508 .collect_vec();
509
510 // Generate getters for fields shared between variants
511 if let Some(accessors_map) = self.module.type_info.accessors.get(name)
512 && !accessors_map.shared_accessors.is_empty()
513 // Don't bother generating shared getters when there's only one variant,
514 // since the specific accessors can always be uses instead.
515 && constructors.len() != 1
516 // Only generate accessors for the API if the constructors are public
517 && constructor_publicity.is_public()
518 {
519 definitions.push(self.shared_custom_type_fields(
520 arena,
521 name,
522 &accessors_map.shared_accessors,
523 ));
524 }
525 // Add start and end position source map trackers to the first and last
526 // definition in place. This is to prevent extra new lines from being added
527 // to the output Since each definition is a separate statement in the output.
528 let start_location = custom_type
529 .documentation
530 .as_ref()
531 // 3 is the length of the "///" documentation marker.
532 // Start is the index of the actual content, so we need to subtract
533 // the length of the marker.
534 .map_or(custom_type.location.start, |(start, _)| *start - 3);
535 let first_definition = definitions.remove(0);
536 definitions.insert(
537 0,
538 self.source_map_tracker(arena, start_location)
539 .append(arena, first_definition),
540 );
541 let last_definition = definitions
542 .pop()
543 .expect("Custom type must have at least one definition here");
544 definitions.push(last_definition.append(
545 arena,
546 self.source_map_tracker(arena, custom_type.end_position),
547 ));
548
549 Some(definitions)
550 }
551
552 fn variant_definition(
553 &self,
554 arena: &'doc DocumentArena<'a, 'doc>,
555 constructor: &'a TypedRecordConstructor,
556 type_name: &'a str,
557 publicity: Publicity,
558 ) -> Document<'a, 'doc> {
559 let class_definition = self.variant_class_definition(arena, constructor, publicity);
560
561 // Singleton constants are used internally by the compiler, so they aren't
562 // part of the public JS API and need to be generated even for private
563 // types.
564 let constructor_singleton = if constructor.arguments.is_empty() {
565 docvec![
566 arena,
567 LINE_DOCUMENT,
568 self.variant_constructor_constant(arena, constructor, type_name, publicity)
569 ]
570 } else {
571 EMPTY_DOCUMENT
572 };
573
574 // If the custom type is private or opaque, we don't need to generate API
575 // functions for it.
576 if publicity.is_private() {
577 return class_definition.append(arena, constructor_singleton);
578 }
579
580 let constructor_definition =
581 self.variant_constructor_definition(arena, constructor, type_name);
582 let variant_check_definition = self.variant_check_definition(arena, constructor, type_name);
583 let fields_definition = self.variant_fields_definition(arena, constructor, type_name);
584
585 docvec![
586 arena,
587 class_definition,
588 constructor_singleton,
589 LINE_DOCUMENT,
590 constructor_definition,
591 LINE_DOCUMENT,
592 variant_check_definition,
593 fields_definition,
594 ]
595 }
596
597 /// Generate a singleton constant for a variant with no fields. This means
598 /// that all values of a particular variant are the same underlying reference,
599 /// allowing them to be compared more efficiently.
600 ///
601 fn variant_constructor_constant(
602 &self,
603 arena: &'doc DocumentArena<'a, 'doc>,
604 constructor: &'a TypedRecordConstructor,
605 type_name: &'a str,
606 publicity: Publicity,
607 ) -> Document<'a, 'doc> {
608 let keyword = if publicity.is_importable() {
609 EXPORT_CONST_SPACE_DOCUMENT
610 } else {
611 CONST_SPACE_DOCUMENT
612 };
613 docvec![
614 arena,
615 self.source_map_tracker(arena, constructor.location.start),
616 keyword,
617 type_name,
618 DOLLAR_DOCUMENT,
619 constructor.name.as_str(),
620 DOLLAR_CONST_DOCUMENT,
621 SPACE_EQUAL_DOCUMENT,
622 docvec![
623 arena,
624 BREAKABLE_SPACE_DOCUMENT,
625 NEW_SPACE_DOCUMENT,
626 constructor.name.as_str(),
627 OPEN_CLOSE_PAREN_SEMICOLON_DOCUMENT,
628 ]
629 .group(arena)
630 .nest(arena, INDENT)
631 ]
632 }
633
634 fn variant_constructor_definition(
635 &self,
636 arena: &'doc DocumentArena<'a, 'doc>,
637 constructor: &'a TypedRecordConstructor,
638 type_name: &'a str,
639 ) -> Document<'a, 'doc> {
640 // If the constructor has no fields, return the singleton constant
641 // instead.
642 if constructor.arguments.is_empty() {
643 return docvec![
644 arena,
645 EXPORT_CONST_SPACE_DOCUMENT,
646 type_name,
647 DOLLAR_DOCUMENT,
648 constructor.name.as_str(),
649 SPACE_EQUAL_OPEN_CLOSE_PAREN_SPACE_ARROW_DOCUMENT,
650 docvec![
651 arena,
652 BREAKABLE_SPACE_DOCUMENT,
653 type_name,
654 DOLLAR_DOCUMENT,
655 constructor.name.as_str(),
656 DOLLAR_CONST_SEMICOLON_DOCUMENT
657 ]
658 .group(arena)
659 .nest(arena, INDENT),
660 ];
661 }
662
663 let mut arguments = Vec::new();
664
665 for (index, parameter) in constructor.arguments.iter().enumerate() {
666 if let Some((_, label)) = ¶meter.label {
667 arguments.push(maybe_escape_identifier(label).to_doc(arena));
668 } else {
669 arguments.push(eco_format!("${index}").to_doc(arena));
670 }
671 }
672
673 let construction = docvec![
674 arena,
675 BREAKABLE_SPACE_DOCUMENT,
676 NEW_SPACE_DOCUMENT,
677 constructor.name.as_str(),
678 OPEN_PAREN_DOCUMENT,
679 arena
680 .join(arguments.clone(), COMMA_BREAK_DOCUMENT)
681 .group(arena),
682 CLOSE_PAREN_SEMICOLON_DOCUMENT,
683 ]
684 .group(arena);
685
686 docvec![
687 arena,
688 self.source_map_tracker(arena, constructor.location.start),
689 EXPORT_CONST_SPACE_DOCUMENT,
690 type_name,
691 DOLLAR_DOCUMENT,
692 constructor.name.as_str(),
693 SPACE_EQUAL_SPACE_OPEN_PAREN_DOCUMENT,
694 arena.join(arguments, COMMA_BREAK_DOCUMENT),
695 CLOSE_PAREN_ARROW_DOCUMENT,
696 construction.nest(arena, INDENT),
697 ]
698 }
699
700 fn variant_check_definition(
701 &self,
702 arena: &'doc DocumentArena<'a, 'doc>,
703 constructor: &'a TypedRecordConstructor,
704 type_name: &'a str,
705 ) -> Document<'a, 'doc> {
706 let construction = docvec![
707 arena,
708 BREAKABLE_SPACE_DOCUMENT,
709 VALUE_INSTANCE_OF_SPACE_DOCUMENT,
710 constructor.name.as_str(),
711 SEMICOLON_DOCUMENT
712 ]
713 .group(arena);
714
715 docvec![
716 arena,
717 self.source_map_tracker(arena, constructor.location.start),
718 EXPORT_CONST_SPACE_DOCUMENT,
719 type_name,
720 DOLLAR_IS_DOCUMENT,
721 constructor.name.as_str(),
722 SPACE_EQUAL_SPACE_VALUE_SPACE_ARROW,
723 construction.nest(arena, INDENT),
724 ]
725 }
726
727 fn variant_fields_definition(
728 &self,
729 arena: &'doc DocumentArena<'a, 'doc>,
730 constructor: &'a TypedRecordConstructor,
731 type_name: &'a str,
732 ) -> Document<'a, 'doc> {
733 let mut functions = Vec::new();
734
735 for (index, argument) in constructor.arguments.iter().enumerate() {
736 // Always generate the accessor for the value at this index. Although
737 // this is not necessary when a label is present, we want to make sure
738 // that adding a label to a record isn't a breaking change. For this
739 // reason, we need to generate an index getter even when a label is
740 // present to ensure consistent behaviour between labelled and unlabelled
741 // field access.
742 let function_name = eco_format!(
743 "{type_name}${record_name}${index}",
744 record_name = constructor.name,
745 );
746
747 let contents;
748
749 // If the argument is labelled, also generate a getter for the labelled
750 // argument.
751 if let Some((_, label)) = &argument.label {
752 let function_name = eco_format!(
753 "{type_name}${record_name}${label}",
754 record_name = constructor.name,
755 );
756
757 contents = docvec![
758 arena,
759 BREAKABLE_SPACE_DOCUMENT,
760 VALUE_DOT_DOCUMENT,
761 maybe_escape_property(label),
762 SEMICOLON_DOCUMENT
763 ]
764 .group(arena);
765
766 functions.push(docvec![
767 arena,
768 LINE_DOCUMENT,
769 self.source_map_tracker(arena, constructor.location.start),
770 EXPORT_CONST_SPACE_DOCUMENT,
771 function_name,
772 SPACE_EQUAL_SPACE_VALUE_SPACE_ARROW,
773 contents.nest(arena, INDENT),
774 ]);
775 } else {
776 contents = docvec![
777 arena,
778 BREAKABLE_SPACE_DOCUMENT,
779 VALUE_OPEN_SQUARE_DOCUMENT,
780 index,
781 CLOSE_SQUARE_SEMICOLON_DOCUMENT
782 ]
783 .group(arena)
784 }
785
786 functions.push(docvec![
787 arena,
788 LINE_DOCUMENT,
789 self.source_map_tracker(arena, constructor.location.start),
790 EXPORT_CONST_SPACE_DOCUMENT,
791 function_name,
792 SPACE_EQUAL_SPACE_VALUE_SPACE_ARROW,
793 contents.nest(arena, INDENT),
794 ]);
795 }
796
797 arena.concat(functions)
798 }
799
800 fn shared_custom_type_fields(
801 &self,
802 arena: &'doc DocumentArena<'a, 'doc>,
803 type_name: &'a str,
804 shared_accessors: &HashMap<EcoString, RecordAccessor>,
805 ) -> Document<'a, 'doc> {
806 let accessors = shared_accessors.keys().sorted().map(|field| {
807 let function_name = eco_format!("{type_name}${field}");
808
809 let contents = docvec![
810 arena,
811 BREAKABLE_SPACE_DOCUMENT,
812 VALUE_DOT_DOCUMENT,
813 maybe_escape_property(field),
814 SEMICOLON_DOCUMENT
815 ]
816 .group(arena);
817
818 docvec![
819 arena,
820 EXPORT_CONST_SPACE_DOCUMENT,
821 function_name,
822 SPACE_EQUAL_SPACE_VALUE_SPACE_ARROW,
823 contents.nest(arena, INDENT),
824 ]
825 });
826 arena.join(accessors, LINE_DOCUMENT)
827 }
828
829 fn variant_class_definition(
830 &self,
831 arena: &'doc DocumentArena<'a, 'doc>,
832 constructor: &'a TypedRecordConstructor,
833 publicity: Publicity,
834 ) -> Document<'a, 'doc> {
835 fn parameter<'a, 'doc>(
836 arena: &'doc DocumentArena<'a, 'doc>,
837 (i, arg): (usize, &TypedRecordConstructorArg),
838 ) -> Document<'a, 'doc> {
839 arg.label
840 .as_ref()
841 .map(|(_, s)| maybe_escape_identifier(s))
842 .unwrap_or_else(|| eco_format!("${i}"))
843 .to_doc(arena)
844 }
845
846 let doc = if let Some((start, documentation)) = &constructor.documentation {
847 docvec![
848 arena,
849 // 3 is the length of the "///" documentation marker.
850 // Start is the index of the actual content, so we need to subtract
851 // the length of the marker.
852 self.source_map_tracker(arena, *start - 3),
853 jsdoc_comment(arena, documentation, publicity),
854 LINE_DOCUMENT
855 ]
856 } else {
857 EMPTY_DOCUMENT
858 };
859
860 let head = if publicity.is_public() {
861 EXPORT_CLASS_SPACE_DOCUMENT
862 } else {
863 CLASS_SPACE_DOCUMENT
864 };
865
866 let head = docvec![
867 arena,
868 self.source_map_tracker(arena, constructor.location.start),
869 head,
870 &constructor.name,
871 SPACE_EXTENDS_CUSTOM_TYPE_OPEN_CURLY_DOCUMENT
872 ];
873
874 if constructor.arguments.is_empty() {
875 return docvec![arena, doc, head, CLOSE_CURLY_DOCUMENT];
876 };
877
878 let parameters = arena.join(
879 constructor
880 .arguments
881 .iter()
882 .enumerate()
883 .map(|argument| parameter(arena, argument)),
884 COMMA_BREAK_DOCUMENT,
885 );
886
887 let constructor_body = arena.join(
888 constructor.arguments.iter().enumerate().map(|(i, arg)| {
889 let var = parameter(arena, (i, arg));
890 match &arg.label {
891 None => docvec![
892 arena,
893 THIS_OPEN_SQUARE_DOCUMENT,
894 i,
895 CLOSE_SQUARE_SPACE_EQUAL_SPACE_DOCUMENT,
896 var,
897 SEMICOLON_DOCUMENT
898 ],
899 Some((_, name)) => {
900 docvec![
901 arena,
902 THIS_DOT_DOCUMENT,
903 maybe_escape_property(name),
904 SPACE_EQUAL_SPACE_DOCUMENT,
905 var,
906 SEMICOLON_DOCUMENT
907 ]
908 }
909 }
910 }),
911 LINE_DOCUMENT,
912 );
913
914 let class_body = docvec![
915 arena,
916 LINE_DOCUMENT,
917 CONSTRUCTOR_OPEN_PAREN_DOCUMENT,
918 parameters,
919 CLOSE_PAREN_SPACE_OPEN_CURLY_DOCUMENT,
920 docvec![
921 arena,
922 LINE_DOCUMENT,
923 SUPER_CALL_SEMICOLON_DOCUMENT,
924 LINE_DOCUMENT,
925 constructor_body
926 ]
927 .nest(arena, INDENT),
928 LINE_DOCUMENT,
929 CLOSE_CURLY_DOCUMENT,
930 ]
931 .nest(arena, INDENT);
932
933 docvec![arena, doc, head, class_body, LINE_DOCUMENT, "}"]
934 }
935
936 fn definitions(&mut self, arena: &'doc DocumentArena<'a, 'doc>) -> Vec<Document<'a, 'doc>> {
937 let mut definitions = vec![];
938
939 for custom_type in &self.module.definitions.custom_types {
940 if let Some(mut new_definitions) = self.custom_type_definition(arena, custom_type) {
941 definitions.append(&mut new_definitions)
942 }
943 }
944
945 for constant in &self.module.definitions.constants {
946 if let Some(definition) = self.module_constant(arena, constant) {
947 definitions.push(definition)
948 }
949 }
950
951 for function in &self.module.definitions.functions {
952 if let Some(definition) = self.module_function(arena, function) {
953 definitions.push(definition)
954 }
955 }
956
957 definitions
958 }
959
960 fn collect_imports(&mut self, arena: &'doc DocumentArena<'a, 'doc>) -> Imports<'a, 'doc> {
961 let mut imports = Imports::new();
962
963 for Import {
964 module,
965 as_name,
966 unqualified_values,
967 package,
968 ..
969 } in &self.module.definitions.imports
970 {
971 self.register_import(
972 arena,
973 &mut imports,
974 package,
975 module,
976 as_name,
977 unqualified_values,
978 );
979 }
980
981 for function in &self.module.definitions.functions {
982 if let Some((_, name)) = &function.name
983 && let Some((module, external_function, _)) = &function.external_javascript
984 {
985 self.register_external_function(
986 arena,
987 &mut imports,
988 function.publicity,
989 name,
990 module,
991 external_function,
992 )
993 }
994 }
995
996 imports
997 }
998
999 fn import_path(&self, package: &str, module: &str) -> EcoString {
1000 // TODO: strip shared prefixed between current module and imported
1001 // module to avoid descending and climbing back out again
1002 if package == self.module.type_info.package || package.is_empty() {
1003 // Same package
1004 match self.current_module_name_segments_count {
1005 1 => eco_format!("./{module}.mjs"),
1006 _ => {
1007 let prefix = "../".repeat(self.current_module_name_segments_count - 1);
1008 eco_format!("{prefix}{module}.mjs")
1009 }
1010 }
1011 } else {
1012 // Different package
1013 let prefix = "../".repeat(self.current_module_name_segments_count);
1014 eco_format!("{prefix}{package}/{module}.mjs")
1015 }
1016 }
1017
1018 fn register_import(
1019 &mut self,
1020 arena: &'doc DocumentArena<'a, 'doc>,
1021 imports: &mut Imports<'a, 'doc>,
1022 package: &'a str,
1023 module: &'a str,
1024 as_name: &Option<(AssignName, SrcSpan)>,
1025 unqualified: &[UnqualifiedImport],
1026 ) {
1027 let get_name = |module: &'a str| {
1028 module
1029 .split('/')
1030 .next_back()
1031 .expect("JavaScript generator could not identify imported module name.")
1032 };
1033
1034 let (discarded, module_name) = match as_name {
1035 None => (false, get_name(module)),
1036 Some((AssignName::Discard(_), _)) => (true, get_name(module)),
1037 Some((AssignName::Variable(name), _)) => (false, name.as_str()),
1038 };
1039
1040 let module_name = eco_format!("${module_name}");
1041 let path = self.import_path(package, module);
1042 let unqualified_imports = unqualified.iter().map(|i| {
1043 let alias = i.as_name.as_ref().map(|n| {
1044 self.register_in_scope(n);
1045 maybe_escape_identifier(n).to_doc(arena)
1046 });
1047 let name = maybe_escape_identifier(&i.name);
1048 Member { name, alias }
1049 });
1050
1051 let aliases = if discarded { vec![] } else { vec![module_name] };
1052 imports.register_module(path, aliases, unqualified_imports);
1053 }
1054
1055 fn register_external_function(
1056 &mut self,
1057 arena: &'doc DocumentArena<'a, 'doc>,
1058 imports: &mut Imports<'a, 'doc>,
1059 publicity: Publicity,
1060 name: &'a str,
1061 module: &'a str,
1062 fun: &EcoString,
1063 ) {
1064 let needs_escaping = !is_usable_js_identifier(name);
1065 let member = Member {
1066 name: fun.clone(),
1067 alias: if name == fun && !needs_escaping {
1068 None
1069 } else if needs_escaping {
1070 Some(escape_identifier(name).to_doc(arena))
1071 } else {
1072 Some(name.to_doc(arena))
1073 },
1074 };
1075 if publicity.is_importable() {
1076 imports.register_export(maybe_escape_identifier_string(name))
1077 }
1078 imports.register_module(EcoString::from(module), [], [member]);
1079 }
1080
1081 fn module_constant(
1082 &mut self,
1083 arena: &'doc DocumentArena<'a, 'doc>,
1084 constant: &'a TypedModuleConstant,
1085 ) -> Option<Document<'a, 'doc>> {
1086 let TypedModuleConstant {
1087 documentation,
1088 location,
1089 publicity,
1090 name,
1091 value,
1092 ..
1093 } = constant;
1094
1095 // We don't generate any code for unused constants.
1096 if self
1097 .module
1098 .unused_definition_positions
1099 .contains(&location.start)
1100 {
1101 return None;
1102 }
1103
1104 let head = if publicity.is_private() {
1105 CONST_SPACE_DOCUMENT
1106 } else {
1107 EXPORT_CONST_SPACE_DOCUMENT
1108 };
1109
1110 let mut generator = expression::Generator::new(
1111 self.module.name.clone(),
1112 self.src_path.clone(),
1113 self.line_numbers,
1114 "".into(),
1115 vec![],
1116 &mut self.tracker,
1117 self.module_scope.clone(),
1118 self.source_map_builder.clone(),
1119 );
1120
1121 let document = generator.constant_expression(arena, Context::Constant, value);
1122
1123 let jsdoc = if let Some((start, documentation)) = documentation {
1124 docvec![
1125 arena,
1126 // 3 is the length of the "///" documentation marker.
1127 // Start is the index of the actual content, so we need to subtract
1128 // the length of the marker.
1129 self.source_map_tracker(arena, *start - 3),
1130 jsdoc_comment(arena, documentation, *publicity),
1131 LINE_DOCUMENT
1132 ]
1133 } else {
1134 EMPTY_DOCUMENT
1135 };
1136
1137 Some(docvec![
1138 arena,
1139 jsdoc,
1140 self.source_map_tracker(arena, location.start),
1141 head,
1142 maybe_escape_identifier(name),
1143 SPACE_EQUAL_SPACE_DOCUMENT,
1144 document,
1145 SEMICOLON_DOCUMENT,
1146 self.source_map_tracker(arena, value.location().end),
1147 ])
1148 }
1149
1150 fn register_in_scope(&mut self, name: &str) {
1151 let _ = self.module_scope.insert(name.into(), 0);
1152 }
1153
1154 fn module_function(
1155 &mut self,
1156 arena: &'doc DocumentArena<'a, 'doc>,
1157 function: &'a TypedFunction,
1158 ) -> Option<Document<'a, 'doc>> {
1159 // We don't generate any code for unused functions.
1160 if self
1161 .module
1162 .unused_definition_positions
1163 .contains(&function.location.start)
1164 {
1165 return None;
1166 }
1167
1168 // If there's an external JavaScript implementation then it will be imported,
1169 // so we don't need to generate a function definition.
1170 if function.external_javascript.is_some() {
1171 return None;
1172 }
1173
1174 // If the function does not support JavaScript then we don't need to generate
1175 // a function definition.
1176 if !function.implementations.supports(Target::JavaScript) {
1177 return None;
1178 }
1179 let function_source_mapping = self.source_map_tracker(arena, function.location.start);
1180
1181 let (_, name) = function
1182 .name
1183 .as_ref()
1184 .expect("A module's function must be named");
1185 let argument_names = function
1186 .arguments
1187 .iter()
1188 .map(|arg| arg.names.get_variable_name())
1189 .collect();
1190
1191 let function_doc = match &function.documentation {
1192 None => EMPTY_DOCUMENT,
1193 Some((start, documentation)) => {
1194 docvec![
1195 arena,
1196 // 3 is the length of the "///" documentation marker.
1197 // Start is the index of the actual content, so we need to subtract
1198 // the length of the marker.
1199 self.source_map_tracker(arena, *start - 3),
1200 jsdoc_comment(arena, documentation, function.publicity),
1201 LINE_DOCUMENT
1202 ]
1203 }
1204 };
1205
1206 let mut generator = expression::Generator::new(
1207 self.module.name.clone(),
1208 self.src_path.clone(),
1209 self.line_numbers,
1210 name.clone(),
1211 argument_names,
1212 &mut self.tracker,
1213 self.module_scope.clone(),
1214 self.source_map_builder.clone(),
1215 );
1216
1217 let head = if function.publicity.is_private() {
1218 FUNCTION_SPACE_DOCUMENT
1219 } else {
1220 EXPORT_FUNCTION_SPACE_DOCUMENT
1221 };
1222
1223 let body = generator.function_body(
1224 arena,
1225 function.body.as_slice(),
1226 function.arguments.as_slice(),
1227 );
1228
1229 Some(docvec![
1230 arena,
1231 function_doc,
1232 function_source_mapping,
1233 head,
1234 maybe_escape_identifier(name.as_str()),
1235 fun_arguments(
1236 arena,
1237 function.arguments.as_slice(),
1238 generator.tail_recursion_used
1239 ),
1240 SPACE_OPEN_CURLY_DOCUMENT,
1241 docvec![arena, LINE_DOCUMENT, body]
1242 .nest(arena, INDENT)
1243 .group(arena),
1244 LINE_DOCUMENT,
1245 CLOSE_CURLY_DOCUMENT,
1246 self.source_map_tracker(arena, function.end_position),
1247 ])
1248 }
1249
1250 fn register_module_definitions_in_scope(&mut self) {
1251 for constant in &self.module.definitions.constants {
1252 self.register_in_scope(&constant.name)
1253 }
1254
1255 for function in &self.module.definitions.functions {
1256 if let Some((_, name)) = &function.name {
1257 self.register_in_scope(name);
1258 }
1259 }
1260
1261 for import in &self.module.definitions.imports {
1262 for unqualified_value in &import.unqualified_values {
1263 self.register_in_scope(unqualified_value.used_name())
1264 }
1265 }
1266 }
1267
1268 fn filepath_definition(&self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> {
1269 if !self.tracker.make_error_used {
1270 return EMPTY_DOCUMENT;
1271 }
1272
1273 docvec![
1274 arena,
1275 CONST_FILEPATH_EQUALS_DOCUMENT,
1276 self.src_path.clone(),
1277 SEMICOLON_DOCUMENT,
1278 TWO_LINES_DOCUMENT
1279 ]
1280 }
1281
1282 fn source_map_tracker(
1283 &self,
1284 arena: &'doc DocumentArena<'a, 'doc>,
1285 start_index: u32,
1286 ) -> Document<'a, 'doc> {
1287 create_cursor_position_observer(
1288 arena,
1289 &self.source_map_builder,
1290 self.line_numbers,
1291 start_index,
1292 )
1293 }
1294}
1295
1296fn jsdoc_comment<'a, 'doc>(
1297 arena: &'doc DocumentArena<'a, 'doc>,
1298 documentation: &EcoString,
1299 publicity: Publicity,
1300) -> Document<'a, 'doc> {
1301 // We start with the documentation of the function
1302 let doc_body = arena.join(
1303 documentation
1304 .trim_end()
1305 .split('\n')
1306 .map(|line| eco_format!(" *{}", line.replace("*/", "*\\/")).to_doc(arena)),
1307 LINE_DOCUMENT,
1308 );
1309
1310 let mut doc = docvec![
1311 arena,
1312 SLASH_TIMES_TIMES_DOCUMENT,
1313 LINE_DOCUMENT,
1314 doc_body,
1315 LINE_DOCUMENT
1316 ];
1317 if !publicity.is_public() {
1318 // If the function is not public we hide the documentation using
1319 // the `@ignore` tag: https://jsdoc.app/tags-ignore
1320 doc = docvec![
1321 arena,
1322 doc,
1323 SPACE_TIMES_SPACE_DOCUMENT,
1324 LINE_DOCUMENT,
1325 SPACE_TIMES_AT_IGNORE_DOCUMENT,
1326 LINE_DOCUMENT
1327 ];
1328 }
1329 // And finally we close the doc comment
1330 docvec![arena, doc, SPACE_TIMES_SLASH_DOCUMENT]
1331}
1332
1333#[derive(Debug)]
1334pub struct ModuleConfig<'a> {
1335 pub module: &'a TypedModule,
1336 pub line_numbers: &'a LineNumbers,
1337 pub src: &'a EcoString,
1338 pub typescript: TypeScriptDeclarations,
1339 pub source_map: bool,
1340 pub stdlib_package: StdlibPackage,
1341 pub path: &'a Utf8Path,
1342 pub project_root: &'a Utf8Path,
1343}
1344
1345pub fn module(config: ModuleConfig<'_>) -> (String, Option<SourceMap>) {
1346 let (output, sourcemap_builder) = {
1347 let arena = DocumentArena::new();
1348 let mut generator = Generator::new(config);
1349 let document = generator.compile(&arena).to_pretty_string(80);
1350 let builder = generator.source_map_builder;
1351 (document, builder)
1352 };
1353
1354 let source_map = sourcemap_builder.map(|builder| {
1355 // We have completed the generation of the module, so we can now take ownership
1356 // of the builder.
1357 Rc::try_unwrap(builder)
1358 .expect("Failed to take ownership of sourcemap builder")
1359 .into_inner()
1360 .0
1361 .into_sourcemap()
1362 });
1363 (output, source_map)
1364}
1365
1366pub fn ts_declaration(module: &TypedModule) -> String {
1367 let arena = DocumentArena::new();
1368 let document = typescript::TypeScriptGenerator::new(module).compile(&arena);
1369 document.to_pretty_string(80)
1370}
1371
1372fn create_cursor_position_observer<'a, 'doc>(
1373 arena: &'doc DocumentArena<'a, 'doc>,
1374 builder: &Option<Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>>,
1375 line_numbers: &LineNumbers,
1376 start_index: u32,
1377) -> Document<'a, 'doc> {
1378 let start_location = line_numbers.line_and_column_number(start_index);
1379 arena.position_observer(match builder {
1380 None => Rc::new(RefCell::new(NullCursorPositionObserver)),
1381 Some(builder) => Rc::new(RefCell::new(SourceMapCursorPositionObserver::new(
1382 start_location,
1383 builder.clone(),
1384 ))),
1385 })
1386}
1387
1388fn fun_arguments<'a, 'doc>(
1389 arena: &'doc DocumentArena<'a, 'doc>,
1390 arguments: &'a [TypedArg],
1391 tail_recursion_used: bool,
1392) -> Document<'a, 'doc> {
1393 let mut discards = 0;
1394 wrap_arguments(
1395 arena,
1396 arguments
1397 .iter()
1398 .map(|argument| match argument.get_variable_name() {
1399 None => {
1400 let doc = if discards == 0 {
1401 UNDERSCORE_DOCUMENT
1402 } else {
1403 eco_format!("_{discards}").to_doc(arena)
1404 };
1405 discards += 1;
1406 doc
1407 }
1408 Some(name) if tail_recursion_used => eco_format!("loop${name}").to_doc(arena),
1409 Some(name) => maybe_escape_identifier(name).to_doc(arena),
1410 }),
1411 )
1412}
1413
1414fn wrap_arguments<'a, 'doc>(
1415 arena: &'doc DocumentArena<'a, 'doc>,
1416 arguments: impl IntoIterator<Item = Document<'a, 'doc>>,
1417) -> Document<'a, 'doc> {
1418 EMPTY_BREAK_DOCUMENT
1419 .append(arena, arena.join(arguments, COMMA_BREAK_DOCUMENT))
1420 .nest(arena, INDENT)
1421 .append(arena, EMPTY_BREAK_DOCUMENT)
1422 .surround(arena, OPEN_PAREN_DOCUMENT, CLOSE_PAREN_DOCUMENT)
1423 .group(arena)
1424}
1425
1426fn wrap_object<'a, 'doc>(
1427 arena: &'doc DocumentArena<'a, 'doc>,
1428 items: impl IntoIterator<Item = (Document<'a, 'doc>, Option<Document<'a, 'doc>>)>,
1429) -> Document<'a, 'doc> {
1430 let mut empty = true;
1431 let fields = items.into_iter().map(|(key, value)| {
1432 empty = false;
1433 match value {
1434 Some(value) => docvec![arena, key, COLON_SPACE_DOCUMENT, value],
1435 None => key.to_doc(arena),
1436 }
1437 });
1438 let fields = arena.join(fields, COMMA_BREAK_DOCUMENT);
1439
1440 if empty {
1441 OPEN_CLOSE_CURLY_DOCUMENT
1442 } else {
1443 docvec![
1444 arena,
1445 docvec![arena, OPEN_CURLY_DOCUMENT, BREAKABLE_SPACE_DOCUMENT, fields]
1446 .nest(arena, INDENT)
1447 .append(arena, BREAKABLE_SPACE_DOCUMENT)
1448 .group(arena),
1449 CLOSE_CURLY_DOCUMENT
1450 ]
1451 }
1452}
1453
1454fn is_usable_js_identifier(word: &str) -> bool {
1455 !matches!(
1456 word,
1457 // Keywords and reserved words
1458 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar
1459 "await"
1460 | "arguments"
1461 | "break"
1462 | "case"
1463 | "catch"
1464 | "class"
1465 | "const"
1466 | "continue"
1467 | "debugger"
1468 | "default"
1469 | "delete"
1470 | "do"
1471 | "else"
1472 | "enum"
1473 | "export"
1474 | "extends"
1475 | "eval"
1476 | "false"
1477 | "finally"
1478 | "for"
1479 | "function"
1480 | "if"
1481 | "implements"
1482 | "import"
1483 | "in"
1484 | "instanceof"
1485 | "interface"
1486 | "let"
1487 | "new"
1488 | "null"
1489 | "package"
1490 | "private"
1491 | "protected"
1492 | "public"
1493 | "return"
1494 | "static"
1495 | "super"
1496 | "switch"
1497 | "this"
1498 | "throw"
1499 | "true"
1500 | "try"
1501 | "typeof"
1502 | "var"
1503 | "void"
1504 | "while"
1505 | "with"
1506 | "yield"
1507 // `undefined` to avoid any unintentional overriding.
1508 | "undefined"
1509 // `then` to avoid a module that defines a `then` function being
1510 // used as a `thenable` in JavaScript when the module is imported
1511 // dynamically, which results in unexpected behaviour.
1512 // It is rather unfortunate that we have to do this.
1513 | "then"
1514 )
1515}
1516
1517fn is_usable_js_property(label: &str) -> bool {
1518 match label {
1519 // `then` to avoid a custom type that defines a `then` function being
1520 // used as a `thenable` in Javascript.
1521 "then"
1522 // `constructor` to avoid unintentional overriding of the constructor of
1523 // records, leading to potential runtime crashes while using `withFields`.
1524 | "constructor"
1525 // `prototype` and `__proto__` to avoid unintentionally overriding the
1526 // prototype chain.
1527 | "prototype" | "__proto__" => false,
1528 _ => true
1529 }
1530}
1531
1532fn maybe_escape_identifier_string(word: &str) -> EcoString {
1533 if is_usable_js_identifier(word) {
1534 EcoString::from(word)
1535 } else {
1536 escape_identifier(word)
1537 }
1538}
1539
1540fn escape_identifier(word: &str) -> EcoString {
1541 eco_format!("{word}$")
1542}
1543
1544fn maybe_escape_identifier(word: &str) -> EcoString {
1545 if is_usable_js_identifier(word) {
1546 EcoString::from(word)
1547 } else {
1548 escape_identifier(word)
1549 }
1550}
1551
1552fn maybe_escape_property(label: &str) -> EcoString {
1553 if is_usable_js_property(label) {
1554 EcoString::from(label)
1555 } else {
1556 escape_identifier(label)
1557 }
1558}
1559
1560#[derive(Debug, Default)]
1561pub(crate) struct UsageTracker {
1562 pub ok_used: bool,
1563 pub list_used: bool,
1564 pub list_empty_class_used: bool,
1565 pub list_empty_const_used: bool,
1566 pub list_non_empty_class_used: bool,
1567 pub prepend_used: bool,
1568 pub error_used: bool,
1569 pub int_remainder_used: bool,
1570 pub make_error_used: bool,
1571 pub custom_type_used: bool,
1572 pub int_division_used: bool,
1573 pub float_division_used: bool,
1574 pub object_equality_used: bool,
1575 pub bit_array_literal_used: bool,
1576 pub bit_array_slice_used: bool,
1577 pub bit_array_slice_to_float_used: bool,
1578 pub bit_array_slice_to_int_used: bool,
1579 pub sized_integer_segment_used: bool,
1580 pub string_bit_array_segment_used: bool,
1581 pub string_utf16_bit_array_segment_used: bool,
1582 pub string_utf32_bit_array_segment_used: bool,
1583 pub codepoint_bit_array_segment_used: bool,
1584 pub codepoint_utf16_bit_array_segment_used: bool,
1585 pub codepoint_utf32_bit_array_segment_used: bool,
1586 pub float_bit_array_segment_used: bool,
1587 pub echo_used: bool,
1588 /// Keeps track of each time a fieldless variant is constructed, so we can
1589 /// import the singleton constant. Optionally contains an alias, if the variant
1590 /// was imported unqualified and aliased.
1591 pub variant_constants_used: HashMap<TypeVariant, Option<EcoString>>,
1592 /// Fieldless variants that are used in `instanceof` checks during pattern
1593 /// matching or comparison. If we're only using a fieldless variant for
1594 /// construction, we don't need to import the variant class itself, just it
1595 /// singleton constant. However, if we are using `instanceof`, we still need
1596 /// to import it.
1597 pub variants_used_in_instanceof: HashSet<TypeVariant>,
1598}
1599
1600#[derive(Debug, PartialEq, Eq, Hash)]
1601pub struct TypeVariant {
1602 package: EcoString,
1603 module: EcoString,
1604 type_name: EcoString,
1605 name: EcoString,
1606}
1607
1608fn bool(bool: bool) -> Document<'static, 'static> {
1609 match bool {
1610 true => TRUE_LOWERCASE_DOCUMENT,
1611 false => FALSE_LOWERCASE_DOCUMENT,
1612 }
1613}
1614
1615/// Int segments <= 48 bits wide in bit arrays are within JavaScript's safe range and are evaluated
1616/// at compile time when all inputs are known. This is done for both bit array expressions and
1617/// pattern matching.
1618///
1619/// Int segments of any size could be evaluated at compile time, but currently aren't due to the
1620/// potential for causing large generated JS for inputs such as `<<0:8192>>`.
1621///
1622pub(crate) const SAFE_INT_SEGMENT_MAX_SIZE: usize = 48;
1623
1624/// Evaluates the value of an Int segment in a bit array into its corresponding bytes. This avoids
1625/// needing to do the evaluation at runtime when all inputs are known at compile-time.
1626///
1627pub(crate) fn bit_array_segment_int_value_to_bytes(
1628 mut value: BigInt,
1629 size: BigInt,
1630 endianness: Endianness,
1631) -> Vec<u8> {
1632 // Clamp negative sizes to zero
1633 let size = size.max(BigInt::ZERO);
1634
1635 // Convert size to u32. This is safe because this function isn't called with a size greater
1636 // than `SAFE_INT_SEGMENT_MAX_SIZE`.
1637 let size = size
1638 .to_u32()
1639 .expect("bit array segment size to be a valid u32");
1640
1641 // Convert negative number to two's complement representation
1642 if value < BigInt::ZERO {
1643 let value_modulus = BigInt::from(2).pow(size);
1644 value = &value_modulus + (value % &value_modulus);
1645 }
1646
1647 // Convert value to the desired number of bytes
1648 let mut bytes = vec![0u8; size as usize / 8];
1649 for byte in bytes.iter_mut() {
1650 *byte = (&value % BigInt::from(256))
1651 .to_u8()
1652 .expect("modulo result to be a valid u32");
1653 value /= BigInt::from(256);
1654 }
1655
1656 if endianness.is_big() {
1657 bytes.reverse();
1658 }
1659
1660 bytes
1661}