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