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