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
55 kB 1661 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2021 The Gleam contributors 3 4mod decision; 5mod expression; 6mod import; 7#[cfg(test)] 8mod tests; 9mod typescript; 10 11use std::cell::RefCell; 12use std::collections::{HashMap, HashSet}; 13use std::rc::Rc; 14 15use debug_ignore::DebugIgnore; 16use num_bigint::BigInt; 17use num_traits::ToPrimitive; 18use sourcemap::SourceMap; 19 20use crate::build::Target; 21use crate::build::package_compiler::StdlibPackage; 22use crate::codegen::TypeScriptDeclarations; 23use crate::line_numbers::LineColumn; 24use crate::type_::{PRELUDE_MODULE_NAME, RecordAccessor}; 25use crate::{ 26 ast::{Import, *}, 27 line_numbers::LineNumbers, 28}; 29use camino::Utf8Path; 30use ecow::{EcoString, eco_format}; 31use expression::Context; 32use itertools::Itertools; 33use pretty_arena::*; 34 35use self::import::{Imports, Member}; 36 37const INDENT: isize = 2; 38 39pub const PRELUDE: &str = include_str!("../templates/prelude.mjs"); 40pub const PRELUDE_TS_DEF: &str = include_str!("../templates/prelude.d.mts"); 41 42#[derive(Debug, Clone, Copy, PartialEq, Eq)] 43pub enum JavaScriptCodegenTarget { 44 JavaScript, 45 TypeScriptDeclarations, 46} 47 48/// A cursor position observer that does nothing. 49/// Mainly used to allow us to create a cursor position observer that does 50/// nothing without having to check if the source map builder is present. 51#[derive(Debug, Clone, Copy)] 52pub struct NullCursorPositionObserver; 53 54impl CursorPositionObserver for NullCursorPositionObserver { 55 fn observe_cursor_position(&mut self, _line: isize, _width: isize) { 56 // Do nothing 57 } 58} 59 60/// A cursor position observer that observes the cursor position and adds it 61/// to the source map builder. When notified it will take the destination 62/// location and map it to the initial source location given when 63/// creating the observer. 64#[derive(Debug)] 65pub struct SourceMapCursorPositionObserver { 66 source_start_location: LineColumn, 67 source_map_builder: Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>, 68} 69 70impl SourceMapCursorPositionObserver { 71 pub fn new( 72 source_start_location: LineColumn, 73 source_map_builder: Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>, 74 ) -> Self { 75 Self { 76 source_start_location, 77 source_map_builder, 78 } 79 } 80} 81 82impl CursorPositionObserver for SourceMapCursorPositionObserver { 83 fn observe_cursor_position(&mut self, line: isize, column: isize) { 84 let _ = self.source_map_builder.borrow_mut().add_raw( 85 line as u32, 86 column as u32, 87 // SourceMapBuilder expects 0-based line and column numbers and we use 1-based ones 88 self.source_start_location.line - 1, 89 self.source_start_location.column - 1, 90 // Since we have a 1-1 mapping between source and destination locations 91 // We always use 0 for the source index. 92 Some(0), 93 None, 94 false, 95 ); 96 } 97} 98 99#[derive(Debug)] 100pub struct Generator<'a> { 101 line_numbers: &'a LineNumbers, 102 module: &'a TypedModule, 103 tracker: UsageTracker, 104 module_scope: im::HashMap<EcoString, usize>, 105 current_module_name_segments_count: usize, 106 typescript: TypeScriptDeclarations, 107 // Debug ignored since SourceMapBuilder doesn't implement debug 108 source_map_builder: Option<Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>>, 109 stdlib_package: StdlibPackage, 110 /// Relative path to the module, surrounded in `"`s to make it a string, and with `\`s escaped 111 /// to `\\`. 112 src_path: EcoString, 113} 114 115impl<'a, 'doc> Generator<'a> { 116 pub fn new(config: ModuleConfig<'a>) -> Self { 117 let ModuleConfig { 118 typescript, 119 source_map, 120 stdlib_package, 121 module, 122 line_numbers, 123 src: _, 124 path: _, 125 project_root, 126 } = config; 127 let current_module_name_segments_count = module.name.split('/').count(); 128 129 let src_path = &module.type_info.src_path; 130 let src_path = src_path 131 .strip_prefix(project_root) 132 .unwrap_or(src_path) 133 .as_str(); 134 let src_path = eco_format!("\"{src_path}\"").replace("\\", "\\\\"); 135 Self { 136 current_module_name_segments_count, 137 line_numbers, 138 module, 139 src_path, 140 tracker: UsageTracker::default(), 141 module_scope: im::HashMap::new(), 142 typescript, 143 source_map_builder: if source_map { 144 let module_name = module.name.clone(); 145 let output_path = format!("{module_name}.mjs"); 146 let module_alias = module_name.split('/').next_back().unwrap_or(&module_name); 147 let input_path = format!("{module_alias}.gleam",); 148 let mut source_map_builder = sourcemap::SourceMapBuilder::new(Some(&output_path)); 149 let _ = source_map_builder.add_source(&input_path); 150 Some(Rc::new(RefCell::new(DebugIgnore(source_map_builder)))) 151 } else { 152 None 153 }, 154 stdlib_package, 155 } 156 } 157 158 fn type_reference(&self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> { 159 if self.typescript == TypeScriptDeclarations::None { 160 return EMPTY_DOCUMENT; 161 } 162 163 // Get the name of the module relative the directory (similar to basename) 164 let module = self 165 .module 166 .name 167 .as_str() 168 .split('/') 169 .next_back() 170 .expect("JavaScript generator could not identify imported module name."); 171 172 docvec![ 173 arena, 174 REFERENCE_TYPES_DOCUMENT, 175 module, 176 DOT_D_DOT_MTS_CLOSE_QUOTE_CLOSE_TAG_DOCUMENT, 177 LINE_DOCUMENT 178 ] 179 } 180 181 fn sourcemap_reference(&self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> { 182 match self.source_map_builder { 183 None => "".to_doc(arena), 184 Some(_) => { 185 // Get the name of the module relative the directory (similar to basename) 186 let module = self 187 .module 188 .name 189 .as_str() 190 .split('/') 191 .next_back() 192 .expect("JavaScript generator could not identify imported module name."); 193 194 docvec![ 195 arena, 196 SOURCE_MAPPING_URL_EQUAL_DOCUMENT, 197 module, 198 DOT_MJS_DOT_MAP_DOCUMENT, 199 LINE_DOCUMENT 200 ] 201 } 202 } 203 } 204 205 pub fn compile(&mut self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> { 206 // Determine what JavaScript imports we need to generate 207 let mut imports = self.collect_imports(arena); 208 209 // Determine what names are defined in the module scope so we know to 210 // rename any variables that are defined within functions using the same 211 // names. 212 self.register_module_definitions_in_scope(); 213 214 // Generate JavaScript code for each statement. 215 let statements = self.definitions(arena); 216 217 // Two lines between each statement 218 let no_statements = statements.is_empty(); 219 let statements = arena.join(statements, TWO_LINES_DOCUMENT); 220 221 // Import any prelude functions that have been used 222 223 if self.tracker.ok_used { 224 self.register_prelude_usage(arena, &mut imports, "Ok", None); 225 }; 226 227 if self.tracker.error_used { 228 self.register_prelude_usage(arena, &mut imports, "Error", None); 229 }; 230 231 if self.tracker.list_used { 232 self.register_prelude_usage(arena, &mut imports, "toList", None); 233 }; 234 235 if self.tracker.list_empty_class_used || self.tracker.echo_used { 236 self.register_prelude_usage(arena, &mut imports, "Empty", Some("$Empty")); 237 }; 238 239 if self.tracker.list_empty_const_used { 240 self.register_prelude_usage( 241 arena, 242 &mut imports, 243 "List$Empty$const", 244 Some("$List$Empty$const"), 245 ); 246 }; 247 248 if self.tracker.list_non_empty_class_used || self.tracker.echo_used { 249 self.register_prelude_usage(arena, &mut imports, "NonEmpty", Some("$NonEmpty")); 250 }; 251 252 if self.tracker.prepend_used { 253 self.register_prelude_usage(arena, &mut imports, "prepend", Some("listPrepend")); 254 }; 255 256 if self.tracker.custom_type_used || self.tracker.echo_used { 257 self.register_prelude_usage(arena, &mut imports, "CustomType", Some("$CustomType")); 258 }; 259 260 if self.tracker.make_error_used { 261 self.register_prelude_usage(arena, &mut imports, "makeError", None); 262 }; 263 264 if self.tracker.int_remainder_used { 265 self.register_prelude_usage(arena, &mut imports, "remainderInt", None); 266 }; 267 268 if self.tracker.float_division_used { 269 self.register_prelude_usage(arena, &mut imports, "divideFloat", None); 270 }; 271 272 if self.tracker.int_division_used { 273 self.register_prelude_usage(arena, &mut imports, "divideInt", None); 274 }; 275 276 if self.tracker.object_equality_used { 277 self.register_prelude_usage(arena, &mut imports, "isEqual", None); 278 }; 279 280 if self.tracker.bit_array_literal_used { 281 self.register_prelude_usage(arena, &mut imports, "toBitArray", None); 282 } 283 284 if self.tracker.bit_array_slice_used || self.tracker.echo_used { 285 self.register_prelude_usage(arena, &mut imports, "bitArraySlice", None); 286 } 287 288 if self.tracker.bit_array_slice_to_float_used { 289 self.register_prelude_usage(arena, &mut imports, "bitArraySliceToFloat", None); 290 } 291 292 if self.tracker.bit_array_slice_to_int_used || self.tracker.echo_used { 293 self.register_prelude_usage(arena, &mut imports, "bitArraySliceToInt", None); 294 } 295 296 if self.tracker.sized_integer_segment_used { 297 self.register_prelude_usage(arena, &mut imports, "sizedInt", None); 298 } 299 300 if self.tracker.string_bit_array_segment_used { 301 self.register_prelude_usage(arena, &mut imports, "stringBits", None); 302 } 303 304 if self.tracker.string_utf16_bit_array_segment_used { 305 self.register_prelude_usage(arena, &mut imports, "stringToUtf16", None); 306 } 307 308 if self.tracker.string_utf32_bit_array_segment_used { 309 self.register_prelude_usage(arena, &mut imports, "stringToUtf32", None); 310 } 311 312 if self.tracker.codepoint_bit_array_segment_used { 313 self.register_prelude_usage(arena, &mut imports, "codepointBits", None); 314 } 315 316 if self.tracker.codepoint_utf16_bit_array_segment_used { 317 self.register_prelude_usage(arena, &mut imports, "codepointToUtf16", None); 318 } 319 320 if self.tracker.codepoint_utf32_bit_array_segment_used { 321 self.register_prelude_usage(arena, &mut imports, "codepointToUtf32", None); 322 } 323 324 if self.tracker.float_bit_array_segment_used { 325 self.register_prelude_usage(arena, &mut imports, "sizedFloat", None); 326 } 327 328 for (variant, alias) in self.tracker.variant_constants_used.iter() { 329 let path = self.import_path(&variant.package, &variant.module); 330 let member = Member { 331 name: eco_format!("{}${}$const", variant.type_name, variant.name), 332 alias: alias.as_ref().map(|alias| alias.to_doc(arena)), 333 }; 334 imports.register_module(path, [], [member]); 335 } 336 337 // If we have some Gleam code that looks something like this: 338 // ```gleam 339 // import option.{None} 340 // 341 // pub fn main() { 342 // None 343 // } 344 // ``` 345 // 346 // Here, the generated JavaScript code for `main` will look something 347 // like this: 348 // 349 // ```javascript 350 // export function main() { 351 // return Option$None$const; 352 // } 353 // ``` 354 // 355 // The `None` variant is technically being imported, but we don't 356 // actually use the generated `None` class in the JavaScript code, so 357 // we don't want to import it. 358 // 359 // However, if we are pattern matching on the `None` variant, there 360 // will be some code that looks like `if (value instanceof None)`, in 361 // which case we still need to import `None`. Therefore we remove all 362 // variants which are used as singleton constants, and that are not used 363 // in pattern matching. 364 // 365 imports.filter_unused_variants( 366 self.tracker 367 .variant_constants_used 368 .keys() 369 .filter(|variant| !self.tracker.variants_used_in_instanceof.contains(variant)) 370 .map(|variant| { 371 let path = self.import_path(&variant.package, &variant.module); 372 (path, variant.name.clone()) 373 }), 374 ); 375 376 let echo_definition = self.echo_definition(arena, &mut imports); 377 let sourcemap_reference = self.sourcemap_reference(arena); 378 let type_reference = self.type_reference(arena); 379 let filepath_definition = self.filepath_definition(arena); 380 381 // Put it all together 382 383 if imports.is_empty() && no_statements { 384 docvec![ 385 arena, 386 sourcemap_reference, 387 type_reference, 388 filepath_definition, 389 EXPORT_SPACE_OPEN_CLOSE_CURLY_DOCUMENT, 390 LINE_DOCUMENT, 391 echo_definition 392 ] 393 } else if imports.is_empty() { 394 docvec![ 395 arena, 396 sourcemap_reference, 397 type_reference, 398 filepath_definition, 399 statements.append(arena, LINE_DOCUMENT), 400 echo_definition 401 ] 402 } else if no_statements { 403 docvec![ 404 arena, 405 sourcemap_reference, 406 type_reference, 407 imports.into_doc(arena, JavaScriptCodegenTarget::JavaScript), 408 filepath_definition, 409 echo_definition, 410 ] 411 } else { 412 docvec![ 413 arena, 414 sourcemap_reference, 415 type_reference, 416 imports.into_doc(arena, JavaScriptCodegenTarget::JavaScript), 417 LINE_DOCUMENT, 418 filepath_definition, 419 statements, 420 LINE_DOCUMENT, 421 echo_definition 422 ] 423 } 424 } 425 426 fn echo_definition( 427 &mut self, 428 arena: &'doc DocumentArena<'a, 'doc>, 429 imports: &mut Imports<'a, 'doc>, 430 ) -> Document<'a, 'doc> { 431 if !self.tracker.echo_used { 432 return EMPTY_DOCUMENT; 433 } 434 435 if StdlibPackage::Present == self.stdlib_package { 436 let value = Some(( 437 AssignName::Variable("stdlib$dict".into()), 438 SrcSpan::default(), 439 )); 440 441 self.register_import(arena, imports, "gleam_stdlib", "gleam/dict", &value, &[]); 442 } 443 self.register_prelude_usage(arena, imports, "BitArray", Some("$BitArray")); 444 self.register_prelude_usage(arena, imports, "List", Some("$List")); 445 self.register_prelude_usage(arena, imports, "UtfCodepoint", Some("$UtfCodepoint")); 446 docvec![ 447 arena, 448 LINE_DOCUMENT, 449 std::include_str!("../templates/echo.mjs"), 450 LINE_DOCUMENT 451 ] 452 } 453 454 fn register_prelude_usage( 455 &self, 456 arena: &'doc DocumentArena<'a, 'doc>, 457 imports: &mut Imports<'a, 'doc>, 458 name: &'static str, 459 alias: Option<&'static str>, 460 ) { 461 let path = self.import_path(&self.module.type_info.package, PRELUDE_MODULE_NAME); 462 let member = Member { 463 name: name.into(), 464 alias: alias.map(|a| a.to_doc(arena)), 465 }; 466 imports.register_module(path, [], [member]); 467 } 468 469 fn custom_type_definition( 470 &mut self, 471 arena: &'doc DocumentArena<'a, 'doc>, 472 custom_type: &'a TypedCustomType, 473 ) -> Option<Vec<Document<'a, 'doc>>> { 474 if self 475 .module 476 .unused_definition_positions 477 .contains(&custom_type.location.start) 478 { 479 return None; 480 } 481 482 let TypedCustomType { 483 name, 484 publicity, 485 constructors, 486 opaque, 487 .. 488 } = custom_type; 489 490 // If there's no constructors then there's nothing to do here. 491 if constructors.is_empty() { 492 return Some(vec![]); 493 } 494 495 self.tracker.custom_type_used = true; 496 497 let constructor_publicity = if *opaque || publicity.is_private() { 498 Publicity::Private 499 } else { 500 Publicity::Public 501 }; 502 503 let mut definitions = constructors 504 .iter() 505 .map(|constructor| { 506 self.variant_definition(arena, constructor, name, constructor_publicity) 507 }) 508 .collect_vec(); 509 510 // Generate getters for fields shared between variants 511 if let Some(accessors_map) = self.module.type_info.accessors.get(name) 512 && !accessors_map.shared_accessors.is_empty() 513 // Don't bother generating shared getters when there's only one variant, 514 // since the specific accessors can always be uses instead. 515 && constructors.len() != 1 516 // Only generate accessors for the API if the constructors are public 517 && constructor_publicity.is_public() 518 { 519 definitions.push(self.shared_custom_type_fields( 520 arena, 521 name, 522 &accessors_map.shared_accessors, 523 )); 524 } 525 // Add start and end position source map trackers to the first and last 526 // definition in place. This is to prevent extra new lines from being added 527 // to the output Since each definition is a separate statement in the output. 528 let start_location = custom_type 529 .documentation 530 .as_ref() 531 // 3 is the length of the "///" documentation marker. 532 // Start is the index of the actual content, so we need to subtract 533 // the length of the marker. 534 .map_or(custom_type.location.start, |(start, _)| *start - 3); 535 let first_definition = definitions.remove(0); 536 definitions.insert( 537 0, 538 self.source_map_tracker(arena, start_location) 539 .append(arena, first_definition), 540 ); 541 let last_definition = definitions 542 .pop() 543 .expect("Custom type must have at least one definition here"); 544 definitions.push(last_definition.append( 545 arena, 546 self.source_map_tracker(arena, custom_type.end_position), 547 )); 548 549 Some(definitions) 550 } 551 552 fn variant_definition( 553 &self, 554 arena: &'doc DocumentArena<'a, 'doc>, 555 constructor: &'a TypedRecordConstructor, 556 type_name: &'a str, 557 publicity: Publicity, 558 ) -> Document<'a, 'doc> { 559 let class_definition = self.variant_class_definition(arena, constructor, publicity); 560 561 // Singleton constants are used internally by the compiler, so they aren't 562 // part of the public JS API and need to be generated even for private 563 // types. 564 let constructor_singleton = if constructor.arguments.is_empty() { 565 docvec![ 566 arena, 567 LINE_DOCUMENT, 568 self.variant_constructor_constant(arena, constructor, type_name, publicity) 569 ] 570 } else { 571 EMPTY_DOCUMENT 572 }; 573 574 // If the custom type is private or opaque, we don't need to generate API 575 // functions for it. 576 if publicity.is_private() { 577 return class_definition.append(arena, constructor_singleton); 578 } 579 580 let constructor_definition = 581 self.variant_constructor_definition(arena, constructor, type_name); 582 let variant_check_definition = self.variant_check_definition(arena, constructor, type_name); 583 let fields_definition = self.variant_fields_definition(arena, constructor, type_name); 584 585 docvec![ 586 arena, 587 class_definition, 588 constructor_singleton, 589 LINE_DOCUMENT, 590 constructor_definition, 591 LINE_DOCUMENT, 592 variant_check_definition, 593 fields_definition, 594 ] 595 } 596 597 /// Generate a singleton constant for a variant with no fields. This means 598 /// that all values of a particular variant are the same underlying reference, 599 /// allowing them to be compared more efficiently. 600 /// 601 fn variant_constructor_constant( 602 &self, 603 arena: &'doc DocumentArena<'a, 'doc>, 604 constructor: &'a TypedRecordConstructor, 605 type_name: &'a str, 606 publicity: Publicity, 607 ) -> Document<'a, 'doc> { 608 let keyword = if publicity.is_importable() { 609 EXPORT_CONST_SPACE_DOCUMENT 610 } else { 611 CONST_SPACE_DOCUMENT 612 }; 613 docvec![ 614 arena, 615 self.source_map_tracker(arena, constructor.location.start), 616 keyword, 617 type_name, 618 DOLLAR_DOCUMENT, 619 constructor.name.as_str(), 620 DOLLAR_CONST_DOCUMENT, 621 SPACE_EQUAL_DOCUMENT, 622 docvec![ 623 arena, 624 BREAKABLE_SPACE_DOCUMENT, 625 NEW_SPACE_DOCUMENT, 626 constructor.name.as_str(), 627 OPEN_CLOSE_PAREN_SEMICOLON_DOCUMENT, 628 ] 629 .group(arena) 630 .nest(arena, INDENT) 631 ] 632 } 633 634 fn variant_constructor_definition( 635 &self, 636 arena: &'doc DocumentArena<'a, 'doc>, 637 constructor: &'a TypedRecordConstructor, 638 type_name: &'a str, 639 ) -> Document<'a, 'doc> { 640 // If the constructor has no fields, return the singleton constant 641 // instead. 642 if constructor.arguments.is_empty() { 643 return docvec![ 644 arena, 645 EXPORT_CONST_SPACE_DOCUMENT, 646 type_name, 647 DOLLAR_DOCUMENT, 648 constructor.name.as_str(), 649 SPACE_EQUAL_OPEN_CLOSE_PAREN_SPACE_ARROW_DOCUMENT, 650 docvec![ 651 arena, 652 BREAKABLE_SPACE_DOCUMENT, 653 type_name, 654 DOLLAR_DOCUMENT, 655 constructor.name.as_str(), 656 DOLLAR_CONST_SEMICOLON_DOCUMENT 657 ] 658 .group(arena) 659 .nest(arena, INDENT), 660 ]; 661 } 662 663 let mut arguments = Vec::new(); 664 665 for (index, parameter) in constructor.arguments.iter().enumerate() { 666 if let Some((_, label)) = &parameter.label { 667 arguments.push(maybe_escape_identifier(label).to_doc(arena)); 668 } else { 669 arguments.push(eco_format!("${index}").to_doc(arena)); 670 } 671 } 672 673 let construction = docvec![ 674 arena, 675 BREAKABLE_SPACE_DOCUMENT, 676 NEW_SPACE_DOCUMENT, 677 constructor.name.as_str(), 678 OPEN_PAREN_DOCUMENT, 679 arena 680 .join(arguments.clone(), COMMA_BREAK_DOCUMENT) 681 .group(arena), 682 CLOSE_PAREN_SEMICOLON_DOCUMENT, 683 ] 684 .group(arena); 685 686 docvec![ 687 arena, 688 self.source_map_tracker(arena, constructor.location.start), 689 EXPORT_CONST_SPACE_DOCUMENT, 690 type_name, 691 DOLLAR_DOCUMENT, 692 constructor.name.as_str(), 693 SPACE_EQUAL_SPACE_OPEN_PAREN_DOCUMENT, 694 arena.join(arguments, COMMA_BREAK_DOCUMENT), 695 CLOSE_PAREN_ARROW_DOCUMENT, 696 construction.nest(arena, INDENT), 697 ] 698 } 699 700 fn variant_check_definition( 701 &self, 702 arena: &'doc DocumentArena<'a, 'doc>, 703 constructor: &'a TypedRecordConstructor, 704 type_name: &'a str, 705 ) -> Document<'a, 'doc> { 706 let construction = docvec![ 707 arena, 708 BREAKABLE_SPACE_DOCUMENT, 709 VALUE_INSTANCE_OF_SPACE_DOCUMENT, 710 constructor.name.as_str(), 711 SEMICOLON_DOCUMENT 712 ] 713 .group(arena); 714 715 docvec![ 716 arena, 717 self.source_map_tracker(arena, constructor.location.start), 718 EXPORT_CONST_SPACE_DOCUMENT, 719 type_name, 720 DOLLAR_IS_DOCUMENT, 721 constructor.name.as_str(), 722 SPACE_EQUAL_SPACE_VALUE_SPACE_ARROW, 723 construction.nest(arena, INDENT), 724 ] 725 } 726 727 fn variant_fields_definition( 728 &self, 729 arena: &'doc DocumentArena<'a, 'doc>, 730 constructor: &'a TypedRecordConstructor, 731 type_name: &'a str, 732 ) -> Document<'a, 'doc> { 733 let mut functions = Vec::new(); 734 735 for (index, argument) in constructor.arguments.iter().enumerate() { 736 // Always generate the accessor for the value at this index. Although 737 // this is not necessary when a label is present, we want to make sure 738 // that adding a label to a record isn't a breaking change. For this 739 // reason, we need to generate an index getter even when a label is 740 // present to ensure consistent behaviour between labelled and unlabelled 741 // field access. 742 let function_name = eco_format!( 743 "{type_name}${record_name}${index}", 744 record_name = constructor.name, 745 ); 746 747 let contents; 748 749 // If the argument is labelled, also generate a getter for the labelled 750 // argument. 751 if let Some((_, label)) = &argument.label { 752 let function_name = eco_format!( 753 "{type_name}${record_name}${label}", 754 record_name = constructor.name, 755 ); 756 757 contents = docvec![ 758 arena, 759 BREAKABLE_SPACE_DOCUMENT, 760 VALUE_DOT_DOCUMENT, 761 maybe_escape_property(label), 762 SEMICOLON_DOCUMENT 763 ] 764 .group(arena); 765 766 functions.push(docvec![ 767 arena, 768 LINE_DOCUMENT, 769 self.source_map_tracker(arena, constructor.location.start), 770 EXPORT_CONST_SPACE_DOCUMENT, 771 function_name, 772 SPACE_EQUAL_SPACE_VALUE_SPACE_ARROW, 773 contents.nest(arena, INDENT), 774 ]); 775 } else { 776 contents = docvec![ 777 arena, 778 BREAKABLE_SPACE_DOCUMENT, 779 VALUE_OPEN_SQUARE_DOCUMENT, 780 index, 781 CLOSE_SQUARE_SEMICOLON_DOCUMENT 782 ] 783 .group(arena) 784 } 785 786 functions.push(docvec![ 787 arena, 788 LINE_DOCUMENT, 789 self.source_map_tracker(arena, constructor.location.start), 790 EXPORT_CONST_SPACE_DOCUMENT, 791 function_name, 792 SPACE_EQUAL_SPACE_VALUE_SPACE_ARROW, 793 contents.nest(arena, INDENT), 794 ]); 795 } 796 797 arena.concat(functions) 798 } 799 800 fn shared_custom_type_fields( 801 &self, 802 arena: &'doc DocumentArena<'a, 'doc>, 803 type_name: &'a str, 804 shared_accessors: &HashMap<EcoString, RecordAccessor>, 805 ) -> Document<'a, 'doc> { 806 let accessors = shared_accessors.keys().sorted().map(|field| { 807 let function_name = eco_format!("{type_name}${field}"); 808 809 let contents = docvec![ 810 arena, 811 BREAKABLE_SPACE_DOCUMENT, 812 VALUE_DOT_DOCUMENT, 813 maybe_escape_property(field), 814 SEMICOLON_DOCUMENT 815 ] 816 .group(arena); 817 818 docvec![ 819 arena, 820 EXPORT_CONST_SPACE_DOCUMENT, 821 function_name, 822 SPACE_EQUAL_SPACE_VALUE_SPACE_ARROW, 823 contents.nest(arena, INDENT), 824 ] 825 }); 826 arena.join(accessors, LINE_DOCUMENT) 827 } 828 829 fn variant_class_definition( 830 &self, 831 arena: &'doc DocumentArena<'a, 'doc>, 832 constructor: &'a TypedRecordConstructor, 833 publicity: Publicity, 834 ) -> Document<'a, 'doc> { 835 fn parameter<'a, 'doc>( 836 arena: &'doc DocumentArena<'a, 'doc>, 837 (i, arg): (usize, &TypedRecordConstructorArg), 838 ) -> Document<'a, 'doc> { 839 arg.label 840 .as_ref() 841 .map(|(_, s)| maybe_escape_identifier(s)) 842 .unwrap_or_else(|| eco_format!("${i}")) 843 .to_doc(arena) 844 } 845 846 let doc = if let Some((start, documentation)) = &constructor.documentation { 847 docvec![ 848 arena, 849 // 3 is the length of the "///" documentation marker. 850 // Start is the index of the actual content, so we need to subtract 851 // the length of the marker. 852 self.source_map_tracker(arena, *start - 3), 853 jsdoc_comment(arena, documentation, publicity), 854 LINE_DOCUMENT 855 ] 856 } else { 857 EMPTY_DOCUMENT 858 }; 859 860 let head = if publicity.is_public() { 861 EXPORT_CLASS_SPACE_DOCUMENT 862 } else { 863 CLASS_SPACE_DOCUMENT 864 }; 865 866 let head = docvec![ 867 arena, 868 self.source_map_tracker(arena, constructor.location.start), 869 head, 870 &constructor.name, 871 SPACE_EXTENDS_CUSTOM_TYPE_OPEN_CURLY_DOCUMENT 872 ]; 873 874 if constructor.arguments.is_empty() { 875 return docvec![arena, doc, head, CLOSE_CURLY_DOCUMENT]; 876 }; 877 878 let parameters = arena.join( 879 constructor 880 .arguments 881 .iter() 882 .enumerate() 883 .map(|argument| parameter(arena, argument)), 884 COMMA_BREAK_DOCUMENT, 885 ); 886 887 let constructor_body = arena.join( 888 constructor.arguments.iter().enumerate().map(|(i, arg)| { 889 let var = parameter(arena, (i, arg)); 890 match &arg.label { 891 None => docvec![ 892 arena, 893 THIS_OPEN_SQUARE_DOCUMENT, 894 i, 895 CLOSE_SQUARE_SPACE_EQUAL_SPACE_DOCUMENT, 896 var, 897 SEMICOLON_DOCUMENT 898 ], 899 Some((_, name)) => { 900 docvec![ 901 arena, 902 THIS_DOT_DOCUMENT, 903 maybe_escape_property(name), 904 SPACE_EQUAL_SPACE_DOCUMENT, 905 var, 906 SEMICOLON_DOCUMENT 907 ] 908 } 909 } 910 }), 911 LINE_DOCUMENT, 912 ); 913 914 let class_body = docvec![ 915 arena, 916 LINE_DOCUMENT, 917 CONSTRUCTOR_OPEN_PAREN_DOCUMENT, 918 parameters, 919 CLOSE_PAREN_SPACE_OPEN_CURLY_DOCUMENT, 920 docvec![ 921 arena, 922 LINE_DOCUMENT, 923 SUPER_CALL_SEMICOLON_DOCUMENT, 924 LINE_DOCUMENT, 925 constructor_body 926 ] 927 .nest(arena, INDENT), 928 LINE_DOCUMENT, 929 CLOSE_CURLY_DOCUMENT, 930 ] 931 .nest(arena, INDENT); 932 933 docvec![arena, doc, head, class_body, LINE_DOCUMENT, "}"] 934 } 935 936 fn definitions(&mut self, arena: &'doc DocumentArena<'a, 'doc>) -> Vec<Document<'a, 'doc>> { 937 let mut definitions = vec![]; 938 939 for custom_type in &self.module.definitions.custom_types { 940 if let Some(mut new_definitions) = self.custom_type_definition(arena, custom_type) { 941 definitions.append(&mut new_definitions) 942 } 943 } 944 945 for constant in &self.module.definitions.constants { 946 if let Some(definition) = self.module_constant(arena, constant) { 947 definitions.push(definition) 948 } 949 } 950 951 for function in &self.module.definitions.functions { 952 if let Some(definition) = self.module_function(arena, function) { 953 definitions.push(definition) 954 } 955 } 956 957 definitions 958 } 959 960 fn collect_imports(&mut self, arena: &'doc DocumentArena<'a, 'doc>) -> Imports<'a, 'doc> { 961 let mut imports = Imports::new(); 962 963 for Import { 964 module, 965 as_name, 966 unqualified_values, 967 package, 968 .. 969 } in &self.module.definitions.imports 970 { 971 self.register_import( 972 arena, 973 &mut imports, 974 package, 975 module, 976 as_name, 977 unqualified_values, 978 ); 979 } 980 981 for function in &self.module.definitions.functions { 982 if let Some((_, name)) = &function.name 983 && let Some((module, external_function, _)) = &function.external_javascript 984 { 985 self.register_external_function( 986 arena, 987 &mut imports, 988 function.publicity, 989 name, 990 module, 991 external_function, 992 ) 993 } 994 } 995 996 imports 997 } 998 999 fn import_path(&self, package: &str, module: &str) -> EcoString { 1000 // TODO: strip shared prefixed between current module and imported 1001 // module to avoid descending and climbing back out again 1002 if package == self.module.type_info.package || package.is_empty() { 1003 // Same package 1004 match self.current_module_name_segments_count { 1005 1 => eco_format!("./{module}.mjs"), 1006 _ => { 1007 let prefix = "../".repeat(self.current_module_name_segments_count - 1); 1008 eco_format!("{prefix}{module}.mjs") 1009 } 1010 } 1011 } else { 1012 // Different package 1013 let prefix = "../".repeat(self.current_module_name_segments_count); 1014 eco_format!("{prefix}{package}/{module}.mjs") 1015 } 1016 } 1017 1018 fn register_import( 1019 &mut self, 1020 arena: &'doc DocumentArena<'a, 'doc>, 1021 imports: &mut Imports<'a, 'doc>, 1022 package: &'a str, 1023 module: &'a str, 1024 as_name: &Option<(AssignName, SrcSpan)>, 1025 unqualified: &[UnqualifiedImport], 1026 ) { 1027 let get_name = |module: &'a str| { 1028 module 1029 .split('/') 1030 .next_back() 1031 .expect("JavaScript generator could not identify imported module name.") 1032 }; 1033 1034 let (discarded, module_name) = match as_name { 1035 None => (false, get_name(module)), 1036 Some((AssignName::Discard(_), _)) => (true, get_name(module)), 1037 Some((AssignName::Variable(name), _)) => (false, name.as_str()), 1038 }; 1039 1040 let module_name = eco_format!("${module_name}"); 1041 let path = self.import_path(package, module); 1042 let unqualified_imports = unqualified.iter().map(|i| { 1043 let alias = i.as_name.as_ref().map(|n| { 1044 self.register_in_scope(n); 1045 maybe_escape_identifier(n).to_doc(arena) 1046 }); 1047 let name = maybe_escape_identifier(&i.name); 1048 Member { name, alias } 1049 }); 1050 1051 let aliases = if discarded { vec![] } else { vec![module_name] }; 1052 imports.register_module(path, aliases, unqualified_imports); 1053 } 1054 1055 fn register_external_function( 1056 &mut self, 1057 arena: &'doc DocumentArena<'a, 'doc>, 1058 imports: &mut Imports<'a, 'doc>, 1059 publicity: Publicity, 1060 name: &'a str, 1061 module: &'a str, 1062 fun: &EcoString, 1063 ) { 1064 let needs_escaping = !is_usable_js_identifier(name); 1065 let member = Member { 1066 name: fun.clone(), 1067 alias: if name == fun && !needs_escaping { 1068 None 1069 } else if needs_escaping { 1070 Some(escape_identifier(name).to_doc(arena)) 1071 } else { 1072 Some(name.to_doc(arena)) 1073 }, 1074 }; 1075 if publicity.is_importable() { 1076 imports.register_export(maybe_escape_identifier_string(name)) 1077 } 1078 imports.register_module(EcoString::from(module), [], [member]); 1079 } 1080 1081 fn module_constant( 1082 &mut self, 1083 arena: &'doc DocumentArena<'a, 'doc>, 1084 constant: &'a TypedModuleConstant, 1085 ) -> Option<Document<'a, 'doc>> { 1086 let TypedModuleConstant { 1087 documentation, 1088 location, 1089 publicity, 1090 name, 1091 value, 1092 .. 1093 } = constant; 1094 1095 // We don't generate any code for unused constants. 1096 if self 1097 .module 1098 .unused_definition_positions 1099 .contains(&location.start) 1100 { 1101 return None; 1102 } 1103 1104 let head = if publicity.is_private() { 1105 CONST_SPACE_DOCUMENT 1106 } else { 1107 EXPORT_CONST_SPACE_DOCUMENT 1108 }; 1109 1110 let mut generator = expression::Generator::new( 1111 self.module.name.clone(), 1112 self.src_path.clone(), 1113 self.line_numbers, 1114 "".into(), 1115 vec![], 1116 &mut self.tracker, 1117 self.module_scope.clone(), 1118 self.source_map_builder.clone(), 1119 ); 1120 1121 let document = generator.constant_expression(arena, Context::Constant, value); 1122 1123 let jsdoc = if let Some((start, documentation)) = documentation { 1124 docvec![ 1125 arena, 1126 // 3 is the length of the "///" documentation marker. 1127 // Start is the index of the actual content, so we need to subtract 1128 // the length of the marker. 1129 self.source_map_tracker(arena, *start - 3), 1130 jsdoc_comment(arena, documentation, *publicity), 1131 LINE_DOCUMENT 1132 ] 1133 } else { 1134 EMPTY_DOCUMENT 1135 }; 1136 1137 Some(docvec![ 1138 arena, 1139 jsdoc, 1140 self.source_map_tracker(arena, location.start), 1141 head, 1142 maybe_escape_identifier(name), 1143 SPACE_EQUAL_SPACE_DOCUMENT, 1144 document, 1145 SEMICOLON_DOCUMENT, 1146 self.source_map_tracker(arena, value.location().end), 1147 ]) 1148 } 1149 1150 fn register_in_scope(&mut self, name: &str) { 1151 let _ = self.module_scope.insert(name.into(), 0); 1152 } 1153 1154 fn module_function( 1155 &mut self, 1156 arena: &'doc DocumentArena<'a, 'doc>, 1157 function: &'a TypedFunction, 1158 ) -> Option<Document<'a, 'doc>> { 1159 // We don't generate any code for unused functions. 1160 if self 1161 .module 1162 .unused_definition_positions 1163 .contains(&function.location.start) 1164 { 1165 return None; 1166 } 1167 1168 // If there's an external JavaScript implementation then it will be imported, 1169 // so we don't need to generate a function definition. 1170 if function.external_javascript.is_some() { 1171 return None; 1172 } 1173 1174 // If the function does not support JavaScript then we don't need to generate 1175 // a function definition. 1176 if !function.implementations.supports(Target::JavaScript) { 1177 return None; 1178 } 1179 let function_source_mapping = self.source_map_tracker(arena, function.location.start); 1180 1181 let (_, name) = function 1182 .name 1183 .as_ref() 1184 .expect("A module's function must be named"); 1185 let argument_names = function 1186 .arguments 1187 .iter() 1188 .map(|arg| arg.names.get_variable_name()) 1189 .collect(); 1190 1191 let function_doc = match &function.documentation { 1192 None => EMPTY_DOCUMENT, 1193 Some((start, documentation)) => { 1194 docvec![ 1195 arena, 1196 // 3 is the length of the "///" documentation marker. 1197 // Start is the index of the actual content, so we need to subtract 1198 // the length of the marker. 1199 self.source_map_tracker(arena, *start - 3), 1200 jsdoc_comment(arena, documentation, function.publicity), 1201 LINE_DOCUMENT 1202 ] 1203 } 1204 }; 1205 1206 let mut generator = expression::Generator::new( 1207 self.module.name.clone(), 1208 self.src_path.clone(), 1209 self.line_numbers, 1210 name.clone(), 1211 argument_names, 1212 &mut self.tracker, 1213 self.module_scope.clone(), 1214 self.source_map_builder.clone(), 1215 ); 1216 1217 let head = if function.publicity.is_private() { 1218 FUNCTION_SPACE_DOCUMENT 1219 } else { 1220 EXPORT_FUNCTION_SPACE_DOCUMENT 1221 }; 1222 1223 let body = generator.function_body( 1224 arena, 1225 function.body.as_slice(), 1226 function.arguments.as_slice(), 1227 ); 1228 1229 Some(docvec![ 1230 arena, 1231 function_doc, 1232 function_source_mapping, 1233 head, 1234 maybe_escape_identifier(name.as_str()), 1235 fun_arguments( 1236 arena, 1237 function.arguments.as_slice(), 1238 generator.tail_recursion_used 1239 ), 1240 SPACE_OPEN_CURLY_DOCUMENT, 1241 docvec![arena, LINE_DOCUMENT, body] 1242 .nest(arena, INDENT) 1243 .group(arena), 1244 LINE_DOCUMENT, 1245 CLOSE_CURLY_DOCUMENT, 1246 self.source_map_tracker(arena, function.end_position), 1247 ]) 1248 } 1249 1250 fn register_module_definitions_in_scope(&mut self) { 1251 for constant in &self.module.definitions.constants { 1252 self.register_in_scope(&constant.name) 1253 } 1254 1255 for function in &self.module.definitions.functions { 1256 if let Some((_, name)) = &function.name { 1257 self.register_in_scope(name); 1258 } 1259 } 1260 1261 for import in &self.module.definitions.imports { 1262 for unqualified_value in &import.unqualified_values { 1263 self.register_in_scope(unqualified_value.used_name()) 1264 } 1265 } 1266 } 1267 1268 fn filepath_definition(&self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> { 1269 if !self.tracker.make_error_used { 1270 return EMPTY_DOCUMENT; 1271 } 1272 1273 docvec![ 1274 arena, 1275 CONST_FILEPATH_EQUALS_DOCUMENT, 1276 self.src_path.clone(), 1277 SEMICOLON_DOCUMENT, 1278 TWO_LINES_DOCUMENT 1279 ] 1280 } 1281 1282 fn source_map_tracker( 1283 &self, 1284 arena: &'doc DocumentArena<'a, 'doc>, 1285 start_index: u32, 1286 ) -> Document<'a, 'doc> { 1287 create_cursor_position_observer( 1288 arena, 1289 &self.source_map_builder, 1290 self.line_numbers, 1291 start_index, 1292 ) 1293 } 1294} 1295 1296fn jsdoc_comment<'a, 'doc>( 1297 arena: &'doc DocumentArena<'a, 'doc>, 1298 documentation: &EcoString, 1299 publicity: Publicity, 1300) -> Document<'a, 'doc> { 1301 // We start with the documentation of the function 1302 let doc_body = arena.join( 1303 documentation 1304 .trim_end() 1305 .split('\n') 1306 .map(|line| eco_format!(" *{}", line.replace("*/", "*\\/")).to_doc(arena)), 1307 LINE_DOCUMENT, 1308 ); 1309 1310 let mut doc = docvec![ 1311 arena, 1312 SLASH_TIMES_TIMES_DOCUMENT, 1313 LINE_DOCUMENT, 1314 doc_body, 1315 LINE_DOCUMENT 1316 ]; 1317 if !publicity.is_public() { 1318 // If the function is not public we hide the documentation using 1319 // the `@ignore` tag: https://jsdoc.app/tags-ignore 1320 doc = docvec![ 1321 arena, 1322 doc, 1323 SPACE_TIMES_SPACE_DOCUMENT, 1324 LINE_DOCUMENT, 1325 SPACE_TIMES_AT_IGNORE_DOCUMENT, 1326 LINE_DOCUMENT 1327 ]; 1328 } 1329 // And finally we close the doc comment 1330 docvec![arena, doc, SPACE_TIMES_SLASH_DOCUMENT] 1331} 1332 1333#[derive(Debug)] 1334pub struct ModuleConfig<'a> { 1335 pub module: &'a TypedModule, 1336 pub line_numbers: &'a LineNumbers, 1337 pub src: &'a EcoString, 1338 pub typescript: TypeScriptDeclarations, 1339 pub source_map: bool, 1340 pub stdlib_package: StdlibPackage, 1341 pub path: &'a Utf8Path, 1342 pub project_root: &'a Utf8Path, 1343} 1344 1345pub fn module(config: ModuleConfig<'_>) -> (String, Option<SourceMap>) { 1346 let (output, sourcemap_builder) = { 1347 let arena = DocumentArena::new(); 1348 let mut generator = Generator::new(config); 1349 let document = generator.compile(&arena).to_pretty_string(80); 1350 let builder = generator.source_map_builder; 1351 (document, builder) 1352 }; 1353 1354 let source_map = sourcemap_builder.map(|builder| { 1355 // We have completed the generation of the module, so we can now take ownership 1356 // of the builder. 1357 Rc::try_unwrap(builder) 1358 .expect("Failed to take ownership of sourcemap builder") 1359 .into_inner() 1360 .0 1361 .into_sourcemap() 1362 }); 1363 (output, source_map) 1364} 1365 1366pub fn ts_declaration(module: &TypedModule) -> String { 1367 let arena = DocumentArena::new(); 1368 let document = typescript::TypeScriptGenerator::new(module).compile(&arena); 1369 document.to_pretty_string(80) 1370} 1371 1372fn create_cursor_position_observer<'a, 'doc>( 1373 arena: &'doc DocumentArena<'a, 'doc>, 1374 builder: &Option<Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>>, 1375 line_numbers: &LineNumbers, 1376 start_index: u32, 1377) -> Document<'a, 'doc> { 1378 let start_location = line_numbers.line_and_column_number(start_index); 1379 arena.position_observer(match builder { 1380 None => Rc::new(RefCell::new(NullCursorPositionObserver)), 1381 Some(builder) => Rc::new(RefCell::new(SourceMapCursorPositionObserver::new( 1382 start_location, 1383 builder.clone(), 1384 ))), 1385 }) 1386} 1387 1388fn fun_arguments<'a, 'doc>( 1389 arena: &'doc DocumentArena<'a, 'doc>, 1390 arguments: &'a [TypedArg], 1391 tail_recursion_used: bool, 1392) -> Document<'a, 'doc> { 1393 let mut discards = 0; 1394 wrap_arguments( 1395 arena, 1396 arguments 1397 .iter() 1398 .map(|argument| match argument.get_variable_name() { 1399 None => { 1400 let doc = if discards == 0 { 1401 UNDERSCORE_DOCUMENT 1402 } else { 1403 eco_format!("_{discards}").to_doc(arena) 1404 }; 1405 discards += 1; 1406 doc 1407 } 1408 Some(name) if tail_recursion_used => eco_format!("loop${name}").to_doc(arena), 1409 Some(name) => maybe_escape_identifier(name).to_doc(arena), 1410 }), 1411 ) 1412} 1413 1414fn wrap_arguments<'a, 'doc>( 1415 arena: &'doc DocumentArena<'a, 'doc>, 1416 arguments: impl IntoIterator<Item = Document<'a, 'doc>>, 1417) -> Document<'a, 'doc> { 1418 EMPTY_BREAK_DOCUMENT 1419 .append(arena, arena.join(arguments, COMMA_BREAK_DOCUMENT)) 1420 .nest(arena, INDENT) 1421 .append(arena, EMPTY_BREAK_DOCUMENT) 1422 .surround(arena, OPEN_PAREN_DOCUMENT, CLOSE_PAREN_DOCUMENT) 1423 .group(arena) 1424} 1425 1426fn wrap_object<'a, 'doc>( 1427 arena: &'doc DocumentArena<'a, 'doc>, 1428 items: impl IntoIterator<Item = (Document<'a, 'doc>, Option<Document<'a, 'doc>>)>, 1429) -> Document<'a, 'doc> { 1430 let mut empty = true; 1431 let fields = items.into_iter().map(|(key, value)| { 1432 empty = false; 1433 match value { 1434 Some(value) => docvec![arena, key, COLON_SPACE_DOCUMENT, value], 1435 None => key.to_doc(arena), 1436 } 1437 }); 1438 let fields = arena.join(fields, COMMA_BREAK_DOCUMENT); 1439 1440 if empty { 1441 OPEN_CLOSE_CURLY_DOCUMENT 1442 } else { 1443 docvec![ 1444 arena, 1445 docvec![arena, OPEN_CURLY_DOCUMENT, BREAKABLE_SPACE_DOCUMENT, fields] 1446 .nest(arena, INDENT) 1447 .append(arena, BREAKABLE_SPACE_DOCUMENT) 1448 .group(arena), 1449 CLOSE_CURLY_DOCUMENT 1450 ] 1451 } 1452} 1453 1454fn is_usable_js_identifier(word: &str) -> bool { 1455 !matches!( 1456 word, 1457 // Keywords and reserved words 1458 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar 1459 "await" 1460 | "arguments" 1461 | "break" 1462 | "case" 1463 | "catch" 1464 | "class" 1465 | "const" 1466 | "continue" 1467 | "debugger" 1468 | "default" 1469 | "delete" 1470 | "do" 1471 | "else" 1472 | "enum" 1473 | "export" 1474 | "extends" 1475 | "eval" 1476 | "false" 1477 | "finally" 1478 | "for" 1479 | "function" 1480 | "if" 1481 | "implements" 1482 | "import" 1483 | "in" 1484 | "instanceof" 1485 | "interface" 1486 | "let" 1487 | "new" 1488 | "null" 1489 | "package" 1490 | "private" 1491 | "protected" 1492 | "public" 1493 | "return" 1494 | "static" 1495 | "super" 1496 | "switch" 1497 | "this" 1498 | "throw" 1499 | "true" 1500 | "try" 1501 | "typeof" 1502 | "var" 1503 | "void" 1504 | "while" 1505 | "with" 1506 | "yield" 1507 // `undefined` to avoid any unintentional overriding. 1508 | "undefined" 1509 // `then` to avoid a module that defines a `then` function being 1510 // used as a `thenable` in JavaScript when the module is imported 1511 // dynamically, which results in unexpected behaviour. 1512 // It is rather unfortunate that we have to do this. 1513 | "then" 1514 ) 1515} 1516 1517fn is_usable_js_property(label: &str) -> bool { 1518 match label { 1519 // `then` to avoid a custom type that defines a `then` function being 1520 // used as a `thenable` in Javascript. 1521 "then" 1522 // `constructor` to avoid unintentional overriding of the constructor of 1523 // records, leading to potential runtime crashes while using `withFields`. 1524 | "constructor" 1525 // `prototype` and `__proto__` to avoid unintentionally overriding the 1526 // prototype chain. 1527 | "prototype" | "__proto__" => false, 1528 _ => true 1529 } 1530} 1531 1532fn maybe_escape_identifier_string(word: &str) -> EcoString { 1533 if is_usable_js_identifier(word) { 1534 EcoString::from(word) 1535 } else { 1536 escape_identifier(word) 1537 } 1538} 1539 1540fn escape_identifier(word: &str) -> EcoString { 1541 eco_format!("{word}$") 1542} 1543 1544fn maybe_escape_identifier(word: &str) -> EcoString { 1545 if is_usable_js_identifier(word) { 1546 EcoString::from(word) 1547 } else { 1548 escape_identifier(word) 1549 } 1550} 1551 1552fn maybe_escape_property(label: &str) -> EcoString { 1553 if is_usable_js_property(label) { 1554 EcoString::from(label) 1555 } else { 1556 escape_identifier(label) 1557 } 1558} 1559 1560#[derive(Debug, Default)] 1561pub(crate) struct UsageTracker { 1562 pub ok_used: bool, 1563 pub list_used: bool, 1564 pub list_empty_class_used: bool, 1565 pub list_empty_const_used: bool, 1566 pub list_non_empty_class_used: bool, 1567 pub prepend_used: bool, 1568 pub error_used: bool, 1569 pub int_remainder_used: bool, 1570 pub make_error_used: bool, 1571 pub custom_type_used: bool, 1572 pub int_division_used: bool, 1573 pub float_division_used: bool, 1574 pub object_equality_used: bool, 1575 pub bit_array_literal_used: bool, 1576 pub bit_array_slice_used: bool, 1577 pub bit_array_slice_to_float_used: bool, 1578 pub bit_array_slice_to_int_used: bool, 1579 pub sized_integer_segment_used: bool, 1580 pub string_bit_array_segment_used: bool, 1581 pub string_utf16_bit_array_segment_used: bool, 1582 pub string_utf32_bit_array_segment_used: bool, 1583 pub codepoint_bit_array_segment_used: bool, 1584 pub codepoint_utf16_bit_array_segment_used: bool, 1585 pub codepoint_utf32_bit_array_segment_used: bool, 1586 pub float_bit_array_segment_used: bool, 1587 pub echo_used: bool, 1588 /// Keeps track of each time a fieldless variant is constructed, so we can 1589 /// import the singleton constant. Optionally contains an alias, if the variant 1590 /// was imported unqualified and aliased. 1591 pub variant_constants_used: HashMap<TypeVariant, Option<EcoString>>, 1592 /// Fieldless variants that are used in `instanceof` checks during pattern 1593 /// matching or comparison. If we're only using a fieldless variant for 1594 /// construction, we don't need to import the variant class itself, just it 1595 /// singleton constant. However, if we are using `instanceof`, we still need 1596 /// to import it. 1597 pub variants_used_in_instanceof: HashSet<TypeVariant>, 1598} 1599 1600#[derive(Debug, PartialEq, Eq, Hash)] 1601pub struct TypeVariant { 1602 package: EcoString, 1603 module: EcoString, 1604 type_name: EcoString, 1605 name: EcoString, 1606} 1607 1608fn bool(bool: bool) -> Document<'static, 'static> { 1609 match bool { 1610 true => TRUE_LOWERCASE_DOCUMENT, 1611 false => FALSE_LOWERCASE_DOCUMENT, 1612 } 1613} 1614 1615/// Int segments <= 48 bits wide in bit arrays are within JavaScript's safe range and are evaluated 1616/// at compile time when all inputs are known. This is done for both bit array expressions and 1617/// pattern matching. 1618/// 1619/// Int segments of any size could be evaluated at compile time, but currently aren't due to the 1620/// potential for causing large generated JS for inputs such as `<<0:8192>>`. 1621/// 1622pub(crate) const SAFE_INT_SEGMENT_MAX_SIZE: usize = 48; 1623 1624/// Evaluates the value of an Int segment in a bit array into its corresponding bytes. This avoids 1625/// needing to do the evaluation at runtime when all inputs are known at compile-time. 1626/// 1627pub(crate) fn bit_array_segment_int_value_to_bytes( 1628 mut value: BigInt, 1629 size: BigInt, 1630 endianness: Endianness, 1631) -> Vec<u8> { 1632 // Clamp negative sizes to zero 1633 let size = size.max(BigInt::ZERO); 1634 1635 // Convert size to u32. This is safe because this function isn't called with a size greater 1636 // than `SAFE_INT_SEGMENT_MAX_SIZE`. 1637 let size = size 1638 .to_u32() 1639 .expect("bit array segment size to be a valid u32"); 1640 1641 // Convert negative number to two's complement representation 1642 if value < BigInt::ZERO { 1643 let value_modulus = BigInt::from(2).pow(size); 1644 value = &value_modulus + (value % &value_modulus); 1645 } 1646 1647 // Convert value to the desired number of bytes 1648 let mut bytes = vec![0u8; size as usize / 8]; 1649 for byte in bytes.iter_mut() { 1650 *byte = (&value % BigInt::from(256)) 1651 .to_u8() 1652 .expect("modulo result to be a valid u32"); 1653 value /= BigInt::from(256); 1654 } 1655 1656 if endianness.is_big() { 1657 bytes.reverse(); 1658 } 1659 1660 bytes 1661}