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