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