Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

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