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