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 1174 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.variant_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 definitions.push(self.shared_custom_type_fields(name, &accessors_map.shared_accessors)); 378 } 379 380 definitions 381 } 382 383 fn variant_definition( 384 &self, 385 constructor: &'a TypedRecordConstructor, 386 type_name: &'a str, 387 publicity: Publicity, 388 ) -> Document<'a> { 389 let class_definition = self.variant_class_definition(constructor, publicity); 390 391 // If the custom type is private or opaque, we don't need to generate API 392 // functions for it. 393 if publicity.is_private() { 394 return class_definition; 395 } 396 397 let constructor_definition = self.variant_constructor_definition(constructor, type_name); 398 let variant_check_definition = self.variant_check_definition(constructor, type_name); 399 let fields_definition = self.variant_fields_definition(constructor, type_name); 400 401 docvec![ 402 class_definition, 403 line(), 404 constructor_definition, 405 line(), 406 variant_check_definition, 407 fields_definition, 408 ] 409 } 410 411 fn variant_constructor_definition( 412 &self, 413 constructor: &'a TypedRecordConstructor, 414 type_name: &'a str, 415 ) -> Document<'a> { 416 let mut arguments = Vec::new(); 417 418 for (index, parameter) in constructor.arguments.iter().enumerate() { 419 if let Some((_, label)) = &parameter.label { 420 arguments.push(maybe_escape_identifier(label).to_doc()); 421 } else { 422 arguments.push(eco_format!("${index}").to_doc()); 423 } 424 } 425 426 let construction = docvec![ 427 break_("", " "), 428 "new ", 429 constructor.name.as_str(), 430 "(", 431 join(arguments.clone(), break_(",", ", ")).group(), 432 ");" 433 ] 434 .group(); 435 436 docvec![ 437 "export const ", 438 type_name, 439 "$", 440 constructor.name.as_str(), 441 " = (", 442 join(arguments, break_(",", ", ")), 443 ") =>", 444 construction.nest(INDENT), 445 ] 446 } 447 448 fn variant_check_definition( 449 &self, 450 constructor: &'a TypedRecordConstructor, 451 type_name: &'a str, 452 ) -> Document<'a> { 453 let construction = docvec![ 454 break_("", " "), 455 "value instanceof ", 456 constructor.name.as_str(), 457 ";" 458 ] 459 .group(); 460 461 docvec![ 462 "export const ", 463 type_name, 464 "$is", 465 constructor.name.as_str(), 466 " = (value) =>", 467 construction.nest(INDENT), 468 ] 469 } 470 471 fn variant_fields_definition( 472 &self, 473 constructor: &'a TypedRecordConstructor, 474 type_name: &'a str, 475 ) -> Document<'a> { 476 let mut functions = Vec::new(); 477 478 for (index, argument) in constructor.arguments.iter().enumerate() { 479 // Always generate the accessor for the value at this index. Although 480 // this is not necessary when a label is present, we want to make sure 481 // that adding a label to a record isn't a breaking change. For this 482 // reason, we need to generate an index getter even when a label is 483 // present to ensure consistent behaviour between labelled and unlabelled 484 // field access. 485 let function_name = eco_format!( 486 "{type_name}${record_name}${index}", 487 record_name = constructor.name, 488 ); 489 490 let contents; 491 492 // If the argument is labelled, also generate a getter for the labelled 493 // argument. 494 if let Some((_, label)) = &argument.label { 495 let function_name = eco_format!( 496 "{type_name}${record_name}${label}", 497 record_name = constructor.name, 498 ); 499 500 contents = 501 docvec![break_("", " "), "value.", maybe_escape_property(label), ";"].group(); 502 503 functions.push(docvec![ 504 line(), 505 "export const ", 506 function_name, 507 " = (value) =>", 508 contents.clone().nest(INDENT), 509 ]); 510 } else { 511 contents = docvec![break_("", " "), "value[", index, "];"].group() 512 } 513 514 functions.push(docvec![ 515 line(), 516 "export const ", 517 function_name, 518 " = (value) =>", 519 contents.nest(INDENT), 520 ]); 521 } 522 523 concat(functions) 524 } 525 526 fn shared_custom_type_fields( 527 &self, 528 type_name: &'a str, 529 shared_accessors: &HashMap<EcoString, RecordAccessor>, 530 ) -> Document<'a> { 531 let accessors = shared_accessors.keys().sorted().map(|field| { 532 let function_name = eco_format!("{type_name}${field}"); 533 534 let contents = 535 docvec![break_("", " "), "value.", maybe_escape_property(field), ";"].group(); 536 537 docvec![ 538 "export const ", 539 function_name, 540 " = (value) =>", 541 contents.nest(INDENT), 542 ] 543 }); 544 concat(Itertools::intersperse(accessors, line())) 545 } 546 547 fn variant_class_definition( 548 &self, 549 constructor: &'a TypedRecordConstructor, 550 publicity: Publicity, 551 ) -> Document<'a> { 552 fn parameter((i, arg): (usize, &TypedRecordConstructorArg)) -> Document<'_> { 553 arg.label 554 .as_ref() 555 .map(|(_, s)| maybe_escape_identifier(s)) 556 .unwrap_or_else(|| eco_format!("${i}")) 557 .to_doc() 558 } 559 560 let doc = if let Some((_, documentation)) = &constructor.documentation { 561 jsdoc_comment(documentation, publicity).append(line()) 562 } else { 563 nil() 564 }; 565 566 let head = if publicity.is_public() { 567 "export class " 568 } else { 569 "class " 570 }; 571 let head = docvec![head, &constructor.name, " extends $CustomType {"]; 572 573 if constructor.arguments.is_empty() { 574 return head.append("}"); 575 }; 576 577 let parameters = join( 578 constructor.arguments.iter().enumerate().map(parameter), 579 break_(",", ", "), 580 ); 581 582 let constructor_body = join( 583 constructor.arguments.iter().enumerate().map(|(i, arg)| { 584 let var = parameter((i, arg)); 585 match &arg.label { 586 None => docvec!["this[", i, "] = ", var, ";"], 587 Some((_, name)) => { 588 docvec!["this.", maybe_escape_property(name), " = ", var, ";"] 589 } 590 } 591 }), 592 line(), 593 ); 594 595 let class_body = docvec![ 596 line(), 597 "constructor(", 598 parameters, 599 ") {", 600 docvec![line(), "super();", line(), constructor_body].nest(INDENT), 601 line(), 602 "}", 603 ] 604 .nest(INDENT); 605 606 docvec![doc, head, class_body, line(), "}"] 607 } 608 609 fn collect_definitions(&mut self) -> Vec<Document<'a>> { 610 self.module 611 .definitions 612 .iter() 613 .flat_map(|definition| match definition { 614 // If a custom type is unused then we don't need to generate code for it 615 Definition::CustomType(CustomType { location, .. }) 616 if self 617 .module 618 .unused_definition_positions 619 .contains(&location.start) => 620 { 621 vec![] 622 } 623 624 Definition::CustomType(CustomType { 625 publicity, 626 constructors, 627 opaque, 628 name, 629 .. 630 }) => self.custom_type_definition(name, constructors, *publicity, *opaque), 631 632 Definition::Function(Function { .. }) 633 | Definition::TypeAlias(TypeAlias { .. }) 634 | Definition::Import(Import { .. }) 635 | Definition::ModuleConstant(ModuleConstant { .. }) => vec![], 636 }) 637 .collect() 638 } 639 640 fn collect_imports(&mut self) -> Imports<'a> { 641 let mut imports = Imports::new(); 642 643 for definition in &self.module.definitions { 644 match definition { 645 Definition::Import(Import { 646 module, 647 as_name, 648 unqualified_values: unqualified, 649 package, 650 .. 651 }) => { 652 self.register_import(&mut imports, package, module, as_name, unqualified); 653 } 654 655 Definition::Function(Function { 656 name: Some((_, name)), 657 publicity, 658 external_javascript: Some((module, function, _location)), 659 .. 660 }) => { 661 self.register_external_function( 662 &mut imports, 663 *publicity, 664 name, 665 module, 666 function, 667 ); 668 } 669 670 Definition::Function(Function { .. }) 671 | Definition::TypeAlias(TypeAlias { .. }) 672 | Definition::CustomType(CustomType { .. }) 673 | Definition::ModuleConstant(ModuleConstant { .. }) => (), 674 } 675 } 676 677 imports 678 } 679 680 fn import_path(&self, package: &'a str, module: &'a str) -> EcoString { 681 // TODO: strip shared prefixed between current module and imported 682 // module to avoid descending and climbing back out again 683 if package == self.module.type_info.package || package.is_empty() { 684 // Same package 685 match self.current_module_name_segments_count { 686 1 => eco_format!("./{module}.mjs"), 687 _ => { 688 let prefix = "../".repeat(self.current_module_name_segments_count - 1); 689 eco_format!("{prefix}{module}.mjs") 690 } 691 } 692 } else { 693 // Different package 694 let prefix = "../".repeat(self.current_module_name_segments_count); 695 eco_format!("{prefix}{package}/{module}.mjs") 696 } 697 } 698 699 fn register_import( 700 &mut self, 701 imports: &mut Imports<'a>, 702 package: &'a str, 703 module: &'a str, 704 as_name: &Option<(AssignName, SrcSpan)>, 705 unqualified: &[UnqualifiedImport], 706 ) { 707 let get_name = |module: &'a str| { 708 module 709 .split('/') 710 .next_back() 711 .expect("JavaScript generator could not identify imported module name.") 712 }; 713 714 let (discarded, module_name) = match as_name { 715 None => (false, get_name(module)), 716 Some((AssignName::Discard(_), _)) => (true, get_name(module)), 717 Some((AssignName::Variable(name), _)) => (false, name.as_str()), 718 }; 719 720 let module_name = eco_format!("${module_name}"); 721 let path = self.import_path(package, module); 722 let unqualified_imports = unqualified.iter().map(|i| { 723 let alias = i.as_name.as_ref().map(|n| { 724 self.register_in_scope(n); 725 maybe_escape_identifier(n).to_doc() 726 }); 727 let name = maybe_escape_identifier(&i.name).to_doc(); 728 Member { name, alias } 729 }); 730 731 let aliases = if discarded { vec![] } else { vec![module_name] }; 732 imports.register_module(path, aliases, unqualified_imports); 733 } 734 735 fn register_external_function( 736 &mut self, 737 imports: &mut Imports<'a>, 738 publicity: Publicity, 739 name: &'a str, 740 module: &'a str, 741 fun: &'a str, 742 ) { 743 let needs_escaping = !is_usable_js_identifier(name); 744 let member = Member { 745 name: fun.to_doc(), 746 alias: if name == fun && !needs_escaping { 747 None 748 } else if needs_escaping { 749 Some(escape_identifier(name).to_doc()) 750 } else { 751 Some(name.to_doc()) 752 }, 753 }; 754 if publicity.is_importable() { 755 imports.register_export(maybe_escape_identifier_string(name)) 756 } 757 imports.register_module(EcoString::from(module), [], [member]); 758 } 759 760 fn module_constant( 761 &mut self, 762 publicity: Publicity, 763 name: &'a EcoString, 764 value: &'a TypedConstant, 765 documentation: &'a Option<(u32, EcoString)>, 766 ) -> Document<'a> { 767 let head = if publicity.is_private() { 768 "const " 769 } else { 770 "export const " 771 }; 772 773 let mut generator = expression::Generator::new( 774 self.module.name.clone(), 775 self.src_path.clone(), 776 self.line_numbers, 777 "".into(), 778 vec![], 779 &mut self.tracker, 780 self.module_scope.clone(), 781 ); 782 783 let document = generator.constant_expression(Context::Constant, value); 784 785 let jsdoc = if let Some((_, documentation)) = documentation { 786 jsdoc_comment(documentation, publicity).append(line()) 787 } else { 788 nil() 789 }; 790 791 docvec![ 792 jsdoc, 793 head, 794 maybe_escape_identifier(name), 795 " = ", 796 document, 797 ";", 798 ] 799 } 800 801 fn register_in_scope(&mut self, name: &str) { 802 let _ = self.module_scope.insert(name.into(), 0); 803 } 804 805 fn module_function(&mut self, function: &'a TypedFunction) -> Document<'a> { 806 let (_, name) = function 807 .name 808 .as_ref() 809 .expect("A module's function must be named"); 810 let argument_names = function 811 .arguments 812 .iter() 813 .map(|arg| arg.names.get_variable_name()) 814 .collect(); 815 let mut generator = expression::Generator::new( 816 self.module.name.clone(), 817 self.src_path.clone(), 818 self.line_numbers, 819 name.clone(), 820 argument_names, 821 &mut self.tracker, 822 self.module_scope.clone(), 823 ); 824 825 let function_doc = match &function.documentation { 826 None => nil(), 827 Some((_, documentation)) => { 828 jsdoc_comment(documentation, function.publicity).append(line()) 829 } 830 }; 831 832 let head = if function.publicity.is_private() { 833 "function " 834 } else { 835 "export function " 836 }; 837 838 let body = generator.function_body(function.body.as_slice(), function.arguments.as_slice()); 839 840 docvec![ 841 function_doc, 842 head, 843 maybe_escape_identifier(name.as_str()), 844 fun_arguments(function.arguments.as_slice(), generator.tail_recursion_used), 845 " {", 846 docvec![line(), body].nest(INDENT).group(), 847 line(), 848 "}", 849 ] 850 } 851 852 fn register_module_definitions_in_scope(&mut self) { 853 for definition in self.module.definitions.iter() { 854 match definition { 855 Definition::ModuleConstant(ModuleConstant { name, .. }) => { 856 self.register_in_scope(name) 857 } 858 859 Definition::Function(Function { name, .. }) => self.register_in_scope( 860 name.as_ref() 861 .map(|(_, name)| name) 862 .expect("Function in a definition must be named"), 863 ), 864 865 Definition::Import(Import { 866 unqualified_values: unqualified, 867 .. 868 }) => unqualified 869 .iter() 870 .for_each(|unq_import| self.register_in_scope(unq_import.used_name())), 871 872 Definition::TypeAlias(TypeAlias { .. }) 873 | Definition::CustomType(CustomType { .. }) => (), 874 } 875 } 876 } 877 878 fn filepath_definition(&self) -> Document<'a> { 879 if !self.tracker.make_error_used { 880 return nil(); 881 } 882 883 docvec!["const FILEPATH = ", self.src_path.clone(), ';', lines(2)] 884 } 885} 886 887fn jsdoc_comment(documentation: &EcoString, publicity: Publicity) -> Document<'_> { 888 let doc_lines = documentation 889 .trim_end() 890 .split('\n') 891 .map(|line| eco_format!(" *{line}", line = line.replace("*/", "*\\/")).to_doc()) 892 .collect_vec(); 893 894 // We start with the documentation of the function 895 let doc_body = join(doc_lines, line()); 896 let mut doc = docvec!["/**", line(), doc_body, line()]; 897 if !publicity.is_public() { 898 // If the function is not public we hide the documentation using 899 // the `@ignore` tag: https://jsdoc.app/tags-ignore 900 doc = docvec![doc, " * ", line(), " * @ignore", line()]; 901 } 902 // And finally we close the doc comment 903 docvec![doc, " */"] 904} 905 906#[derive(Debug)] 907pub struct ModuleConfig<'a> { 908 pub module: &'a TypedModule, 909 pub line_numbers: &'a LineNumbers, 910 pub src: &'a EcoString, 911 pub typescript: TypeScriptDeclarations, 912 pub stdlib_package: StdlibPackage, 913 pub path: &'a Utf8Path, 914 pub project_root: &'a Utf8Path, 915} 916 917pub fn module(config: ModuleConfig<'_>) -> String { 918 let document = Generator::new(config).compile(); 919 document.to_pretty_string(80) 920} 921 922pub fn ts_declaration(module: &TypedModule) -> String { 923 let document = typescript::TypeScriptGenerator::new(module).compile(); 924 document.to_pretty_string(80) 925} 926 927fn fun_arguments(arguments: &'_ [TypedArg], tail_recursion_used: bool) -> Document<'_> { 928 let mut discards = 0; 929 wrap_arguments( 930 arguments 931 .iter() 932 .map(|argument| match argument.get_variable_name() { 933 None => { 934 let doc = if discards == 0 { 935 "_".to_doc() 936 } else { 937 eco_format!("_{discards}").to_doc() 938 }; 939 discards += 1; 940 doc 941 } 942 Some(name) if tail_recursion_used => eco_format!("loop${name}").to_doc(), 943 Some(name) => maybe_escape_identifier(name).to_doc(), 944 }), 945 ) 946} 947 948fn wrap_arguments<'a, I>(arguments: I) -> Document<'a> 949where 950 I: IntoIterator<Item = Document<'a>>, 951{ 952 break_("", "") 953 .append(join(arguments, break_(",", ", "))) 954 .nest(INDENT) 955 .append(break_("", "")) 956 .surround("(", ")") 957 .group() 958} 959 960fn wrap_object<'a>( 961 items: impl IntoIterator<Item = (Document<'a>, Option<Document<'a>>)>, 962) -> Document<'a> { 963 let mut empty = true; 964 let fields = items.into_iter().map(|(key, value)| { 965 empty = false; 966 match value { 967 Some(value) => docvec![key, ": ", value], 968 None => key.to_doc(), 969 } 970 }); 971 let fields = join(fields, break_(",", ", ")); 972 973 if empty { 974 "{}".to_doc() 975 } else { 976 docvec![ 977 docvec!["{", break_("", " "), fields] 978 .nest(INDENT) 979 .append(break_("", " ")) 980 .group(), 981 "}" 982 ] 983 } 984} 985 986fn is_usable_js_identifier(word: &str) -> bool { 987 !matches!( 988 word, 989 // Keywords and reserved words 990 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar 991 "await" 992 | "arguments" 993 | "break" 994 | "case" 995 | "catch" 996 | "class" 997 | "const" 998 | "continue" 999 | "debugger" 1000 | "default" 1001 | "delete" 1002 | "do" 1003 | "else" 1004 | "enum" 1005 | "export" 1006 | "extends" 1007 | "eval" 1008 | "false" 1009 | "finally" 1010 | "for" 1011 | "function" 1012 | "if" 1013 | "implements" 1014 | "import" 1015 | "in" 1016 | "instanceof" 1017 | "interface" 1018 | "let" 1019 | "new" 1020 | "null" 1021 | "package" 1022 | "private" 1023 | "protected" 1024 | "public" 1025 | "return" 1026 | "static" 1027 | "super" 1028 | "switch" 1029 | "this" 1030 | "throw" 1031 | "true" 1032 | "try" 1033 | "typeof" 1034 | "var" 1035 | "void" 1036 | "while" 1037 | "with" 1038 | "yield" 1039 // `undefined` to avoid any unintentional overriding. 1040 | "undefined" 1041 // `then` to avoid a module that defines a `then` function being 1042 // used as a `thenable` in JavaScript when the module is imported 1043 // dynamically, which results in unexpected behaviour. 1044 // It is rather unfortunate that we have to do this. 1045 | "then" 1046 ) 1047} 1048 1049fn is_usable_js_property(label: &str) -> bool { 1050 match label { 1051 // `then` to avoid a custom type that defines a `then` function being 1052 // used as a `thenable` in Javascript. 1053 "then" 1054 // `constructor` to avoid unintentional overriding of the constructor of 1055 // records, leading to potential runtime crashes while using `withFields`. 1056 | "constructor" 1057 // `prototype` and `__proto__` to avoid unintentionally overriding the 1058 // prototype chain. 1059 | "prototype" | "__proto__" => false, 1060 _ => true 1061 } 1062} 1063 1064fn maybe_escape_identifier_string(word: &str) -> EcoString { 1065 if is_usable_js_identifier(word) { 1066 EcoString::from(word) 1067 } else { 1068 escape_identifier(word) 1069 } 1070} 1071 1072fn escape_identifier(word: &str) -> EcoString { 1073 eco_format!("{word}$") 1074} 1075 1076fn maybe_escape_identifier(word: &str) -> EcoString { 1077 if is_usable_js_identifier(word) { 1078 EcoString::from(word) 1079 } else { 1080 escape_identifier(word) 1081 } 1082} 1083 1084fn maybe_escape_property(label: &str) -> EcoString { 1085 if is_usable_js_property(label) { 1086 EcoString::from(label) 1087 } else { 1088 escape_identifier(label) 1089 } 1090} 1091 1092#[derive(Debug, Default)] 1093pub(crate) struct UsageTracker { 1094 pub ok_used: bool, 1095 pub list_used: bool, 1096 pub list_empty_class_used: bool, 1097 pub list_non_empty_class_used: bool, 1098 pub prepend_used: bool, 1099 pub error_used: bool, 1100 pub int_remainder_used: bool, 1101 pub make_error_used: bool, 1102 pub custom_type_used: bool, 1103 pub int_division_used: bool, 1104 pub float_division_used: bool, 1105 pub object_equality_used: bool, 1106 pub bit_array_literal_used: bool, 1107 pub bit_array_slice_used: bool, 1108 pub bit_array_slice_to_float_used: bool, 1109 pub bit_array_slice_to_int_used: bool, 1110 pub sized_integer_segment_used: bool, 1111 pub string_bit_array_segment_used: bool, 1112 pub string_utf16_bit_array_segment_used: bool, 1113 pub string_utf32_bit_array_segment_used: bool, 1114 pub codepoint_bit_array_segment_used: bool, 1115 pub codepoint_utf16_bit_array_segment_used: bool, 1116 pub codepoint_utf32_bit_array_segment_used: bool, 1117 pub float_bit_array_segment_used: bool, 1118 pub echo_used: bool, 1119} 1120 1121fn bool(bool: bool) -> Document<'static> { 1122 match bool { 1123 true => "true".to_doc(), 1124 false => "false".to_doc(), 1125 } 1126} 1127 1128/// Int segments <= 48 bits wide in bit arrays are within JavaScript's safe range and are evaluated 1129/// at compile time when all inputs are known. This is done for both bit array expressions and 1130/// pattern matching. 1131/// 1132/// Int segments of any size could be evaluated at compile time, but currently aren't due to the 1133/// potential for causing large generated JS for inputs such as `<<0:8192>>`. 1134/// 1135pub(crate) const SAFE_INT_SEGMENT_MAX_SIZE: usize = 48; 1136 1137/// Evaluates the value of an Int segment in a bit array into its corresponding bytes. This avoids 1138/// needing to do the evaluation at runtime when all inputs are known at compile-time. 1139/// 1140pub(crate) fn bit_array_segment_int_value_to_bytes( 1141 mut value: BigInt, 1142 size: BigInt, 1143 endianness: Endianness, 1144) -> Vec<u8> { 1145 // Clamp negative sizes to zero 1146 let size = size.max(BigInt::ZERO); 1147 1148 // Convert size to u32. This is safe because this function isn't called with a size greater 1149 // than `SAFE_INT_SEGMENT_MAX_SIZE`. 1150 let size = size 1151 .to_u32() 1152 .expect("bit array segment size to be a valid u32"); 1153 1154 // Convert negative number to two's complement representation 1155 if value < BigInt::ZERO { 1156 let value_modulus = BigInt::from(2).pow(size); 1157 value = &value_modulus + (value % &value_modulus); 1158 } 1159 1160 // Convert value to the desired number of bytes 1161 let mut bytes = vec![0u8; size as usize / 8]; 1162 for byte in bytes.iter_mut() { 1163 *byte = (&value % BigInt::from(256)) 1164 .to_u8() 1165 .expect("modulo result to be a valid u32"); 1166 value /= BigInt::from(256); 1167 } 1168 1169 if endianness.is_big() { 1170 bytes.reverse(); 1171 } 1172 1173 bytes 1174}