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
56 kB 1667 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 // Each comment line is turned into a zero width string. 1307 // We actually don't need to know how long a comment string is 1308 // because comments will never be split anyway. 1309 // So this saves us a lot of useless grapheme counting that would 1310 // otherwise have to happen if we just turned the string into a 1311 // regular document. 1312 .map(|line| arena.zero_width_string(eco_format!(" *{}", line.replace("*/", "*\\/")))), 1313 LINE_DOCUMENT, 1314 ); 1315 1316 let mut doc = docvec![ 1317 arena, 1318 SLASH_TIMES_TIMES_DOCUMENT, 1319 LINE_DOCUMENT, 1320 doc_body, 1321 LINE_DOCUMENT 1322 ]; 1323 if !publicity.is_public() { 1324 // If the function is not public we hide the documentation using 1325 // the `@ignore` tag: https://jsdoc.app/tags-ignore 1326 doc = docvec![ 1327 arena, 1328 doc, 1329 SPACE_TIMES_SPACE_DOCUMENT, 1330 LINE_DOCUMENT, 1331 SPACE_TIMES_AT_IGNORE_DOCUMENT, 1332 LINE_DOCUMENT 1333 ]; 1334 } 1335 // And finally we close the doc comment 1336 docvec![arena, doc, SPACE_TIMES_SLASH_DOCUMENT] 1337} 1338 1339#[derive(Debug)] 1340pub struct ModuleConfig<'a> { 1341 pub module: &'a TypedModule, 1342 pub line_numbers: &'a LineNumbers, 1343 pub src: &'a EcoString, 1344 pub typescript: TypeScriptDeclarations, 1345 pub source_map: bool, 1346 pub stdlib_package: StdlibPackage, 1347 pub path: &'a Utf8Path, 1348 pub project_root: &'a Utf8Path, 1349} 1350 1351pub fn module(config: ModuleConfig<'_>) -> (String, Option<SourceMap>) { 1352 let (output, sourcemap_builder) = { 1353 let arena = DocumentArena::new(); 1354 let mut generator = Generator::new(config); 1355 let document = generator.compile(&arena).to_pretty_string(80); 1356 let builder = generator.source_map_builder; 1357 (document, builder) 1358 }; 1359 1360 let source_map = sourcemap_builder.map(|builder| { 1361 // We have completed the generation of the module, so we can now take ownership 1362 // of the builder. 1363 Rc::try_unwrap(builder) 1364 .expect("Failed to take ownership of sourcemap builder") 1365 .into_inner() 1366 .0 1367 .into_sourcemap() 1368 }); 1369 (output, source_map) 1370} 1371 1372pub fn ts_declaration(module: &TypedModule) -> String { 1373 let arena = DocumentArena::new(); 1374 let document = typescript::TypeScriptGenerator::new(module).compile(&arena); 1375 document.to_pretty_string(80) 1376} 1377 1378fn create_cursor_position_observer<'a, 'doc>( 1379 arena: &'doc DocumentArena<'a, 'doc>, 1380 builder: &Option<Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>>, 1381 line_numbers: &LineNumbers, 1382 start_index: u32, 1383) -> Document<'a, 'doc> { 1384 let start_location = line_numbers.line_and_column_number(start_index); 1385 arena.position_observer(match builder { 1386 None => Rc::new(RefCell::new(NullCursorPositionObserver)), 1387 Some(builder) => Rc::new(RefCell::new(SourceMapCursorPositionObserver::new( 1388 start_location, 1389 builder.clone(), 1390 ))), 1391 }) 1392} 1393 1394fn fun_arguments<'a, 'doc>( 1395 arena: &'doc DocumentArena<'a, 'doc>, 1396 arguments: &'a [TypedArg], 1397 tail_recursion_used: bool, 1398) -> Document<'a, 'doc> { 1399 let mut discards = 0; 1400 wrap_arguments( 1401 arena, 1402 arguments 1403 .iter() 1404 .map(|argument| match argument.get_variable_name() { 1405 None => { 1406 let doc = if discards == 0 { 1407 UNDERSCORE_DOCUMENT 1408 } else { 1409 eco_format!("_{discards}").to_doc(arena) 1410 }; 1411 discards += 1; 1412 doc 1413 } 1414 Some(name) if tail_recursion_used => eco_format!("loop${name}").to_doc(arena), 1415 Some(name) => maybe_escape_identifier(name).to_doc(arena), 1416 }), 1417 ) 1418} 1419 1420fn wrap_arguments<'a, 'doc>( 1421 arena: &'doc DocumentArena<'a, 'doc>, 1422 arguments: impl IntoIterator<Item = Document<'a, 'doc>>, 1423) -> Document<'a, 'doc> { 1424 EMPTY_BREAK_DOCUMENT 1425 .append(arena, arena.join(arguments, COMMA_BREAK_DOCUMENT)) 1426 .nest(arena, INDENT) 1427 .append(arena, EMPTY_BREAK_DOCUMENT) 1428 .surround(arena, OPEN_PAREN_DOCUMENT, CLOSE_PAREN_DOCUMENT) 1429 .group(arena) 1430} 1431 1432fn wrap_object<'a, 'doc>( 1433 arena: &'doc DocumentArena<'a, 'doc>, 1434 items: impl IntoIterator<Item = (Document<'a, 'doc>, Option<Document<'a, 'doc>>)>, 1435) -> Document<'a, 'doc> { 1436 let mut empty = true; 1437 let fields = items.into_iter().map(|(key, value)| { 1438 empty = false; 1439 match value { 1440 Some(value) => docvec![arena, key, COLON_SPACE_DOCUMENT, value], 1441 None => key.to_doc(arena), 1442 } 1443 }); 1444 let fields = arena.join(fields, COMMA_BREAK_DOCUMENT); 1445 1446 if empty { 1447 OPEN_CLOSE_CURLY_DOCUMENT 1448 } else { 1449 docvec![ 1450 arena, 1451 docvec![arena, OPEN_CURLY_DOCUMENT, BREAKABLE_SPACE_DOCUMENT, fields] 1452 .nest(arena, INDENT) 1453 .append(arena, BREAKABLE_SPACE_DOCUMENT) 1454 .group(arena), 1455 CLOSE_CURLY_DOCUMENT 1456 ] 1457 } 1458} 1459 1460fn is_usable_js_identifier(word: &str) -> bool { 1461 !matches!( 1462 word, 1463 // Keywords and reserved words 1464 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar 1465 "await" 1466 | "arguments" 1467 | "break" 1468 | "case" 1469 | "catch" 1470 | "class" 1471 | "const" 1472 | "continue" 1473 | "debugger" 1474 | "default" 1475 | "delete" 1476 | "do" 1477 | "else" 1478 | "enum" 1479 | "export" 1480 | "extends" 1481 | "eval" 1482 | "false" 1483 | "finally" 1484 | "for" 1485 | "function" 1486 | "if" 1487 | "implements" 1488 | "import" 1489 | "in" 1490 | "instanceof" 1491 | "interface" 1492 | "let" 1493 | "new" 1494 | "null" 1495 | "package" 1496 | "private" 1497 | "protected" 1498 | "public" 1499 | "return" 1500 | "static" 1501 | "super" 1502 | "switch" 1503 | "this" 1504 | "throw" 1505 | "true" 1506 | "try" 1507 | "typeof" 1508 | "var" 1509 | "void" 1510 | "while" 1511 | "with" 1512 | "yield" 1513 // `undefined` to avoid any unintentional overriding. 1514 | "undefined" 1515 // `then` to avoid a module that defines a `then` function being 1516 // used as a `thenable` in JavaScript when the module is imported 1517 // dynamically, which results in unexpected behaviour. 1518 // It is rather unfortunate that we have to do this. 1519 | "then" 1520 ) 1521} 1522 1523fn is_usable_js_property(label: &str) -> bool { 1524 match label { 1525 // `then` to avoid a custom type that defines a `then` function being 1526 // used as a `thenable` in Javascript. 1527 "then" 1528 // `constructor` to avoid unintentional overriding of the constructor of 1529 // records, leading to potential runtime crashes while using `withFields`. 1530 | "constructor" 1531 // `prototype` and `__proto__` to avoid unintentionally overriding the 1532 // prototype chain. 1533 | "prototype" | "__proto__" => false, 1534 _ => true 1535 } 1536} 1537 1538fn maybe_escape_identifier_string(word: &str) -> EcoString { 1539 if is_usable_js_identifier(word) { 1540 EcoString::from(word) 1541 } else { 1542 escape_identifier(word) 1543 } 1544} 1545 1546fn escape_identifier(word: &str) -> EcoString { 1547 eco_format!("{word}$") 1548} 1549 1550fn maybe_escape_identifier(word: &str) -> EcoString { 1551 if is_usable_js_identifier(word) { 1552 EcoString::from(word) 1553 } else { 1554 escape_identifier(word) 1555 } 1556} 1557 1558fn maybe_escape_property(label: &str) -> EcoString { 1559 if is_usable_js_property(label) { 1560 EcoString::from(label) 1561 } else { 1562 escape_identifier(label) 1563 } 1564} 1565 1566#[derive(Debug, Default)] 1567pub(crate) struct UsageTracker { 1568 pub ok_used: bool, 1569 pub list_used: bool, 1570 pub list_empty_class_used: bool, 1571 pub list_empty_const_used: bool, 1572 pub list_non_empty_class_used: bool, 1573 pub prepend_used: bool, 1574 pub error_used: bool, 1575 pub int_remainder_used: bool, 1576 pub make_error_used: bool, 1577 pub custom_type_used: bool, 1578 pub int_division_used: bool, 1579 pub float_division_used: bool, 1580 pub object_equality_used: bool, 1581 pub bit_array_literal_used: bool, 1582 pub bit_array_slice_used: bool, 1583 pub bit_array_slice_to_float_used: bool, 1584 pub bit_array_slice_to_int_used: bool, 1585 pub sized_integer_segment_used: bool, 1586 pub string_bit_array_segment_used: bool, 1587 pub string_utf16_bit_array_segment_used: bool, 1588 pub string_utf32_bit_array_segment_used: bool, 1589 pub codepoint_bit_array_segment_used: bool, 1590 pub codepoint_utf16_bit_array_segment_used: bool, 1591 pub codepoint_utf32_bit_array_segment_used: bool, 1592 pub float_bit_array_segment_used: bool, 1593 pub echo_used: bool, 1594 /// Keeps track of each time a fieldless variant is constructed, so we can 1595 /// import the singleton constant. Optionally contains an alias, if the variant 1596 /// was imported unqualified and aliased. 1597 pub variant_constants_used: HashMap<TypeVariant, Option<EcoString>>, 1598 /// Fieldless variants that are used in `instanceof` checks during pattern 1599 /// matching or comparison. If we're only using a fieldless variant for 1600 /// construction, we don't need to import the variant class itself, just it 1601 /// singleton constant. However, if we are using `instanceof`, we still need 1602 /// to import it. 1603 pub variants_used_in_instanceof: HashSet<TypeVariant>, 1604} 1605 1606#[derive(Debug, PartialEq, Eq, Hash)] 1607pub struct TypeVariant { 1608 package: EcoString, 1609 module: EcoString, 1610 type_name: EcoString, 1611 name: EcoString, 1612} 1613 1614fn bool(bool: bool) -> Document<'static, 'static> { 1615 match bool { 1616 true => TRUE_LOWERCASE_DOCUMENT, 1617 false => FALSE_LOWERCASE_DOCUMENT, 1618 } 1619} 1620 1621/// Int segments <= 48 bits wide in bit arrays are within JavaScript's safe range and are evaluated 1622/// at compile time when all inputs are known. This is done for both bit array expressions and 1623/// pattern matching. 1624/// 1625/// Int segments of any size could be evaluated at compile time, but currently aren't due to the 1626/// potential for causing large generated JS for inputs such as `<<0:8192>>`. 1627/// 1628pub(crate) const SAFE_INT_SEGMENT_MAX_SIZE: usize = 48; 1629 1630/// Evaluates the value of an Int segment in a bit array into its corresponding bytes. This avoids 1631/// needing to do the evaluation at runtime when all inputs are known at compile-time. 1632/// 1633pub(crate) fn bit_array_segment_int_value_to_bytes( 1634 mut value: BigInt, 1635 size: BigInt, 1636 endianness: Endianness, 1637) -> Vec<u8> { 1638 // Clamp negative sizes to zero 1639 let size = size.max(BigInt::ZERO); 1640 1641 // Convert size to u32. This is safe because this function isn't called with a size greater 1642 // than `SAFE_INT_SEGMENT_MAX_SIZE`. 1643 let size = size 1644 .to_u32() 1645 .expect("bit array segment size to be a valid u32"); 1646 1647 // Convert negative number to two's complement representation 1648 if value < BigInt::ZERO { 1649 let value_modulus = BigInt::from(2).pow(size); 1650 value = &value_modulus + (value % &value_modulus); 1651 } 1652 1653 // Convert value to the desired number of bytes 1654 let mut bytes = vec![0u8; size as usize / 8]; 1655 for byte in bytes.iter_mut() { 1656 *byte = (&value % BigInt::from(256)) 1657 .to_u8() 1658 .expect("modulo result to be a valid u32"); 1659 value /= BigInt::from(256); 1660 } 1661 1662 if endianness.is_big() { 1663 bytes.reverse(); 1664 } 1665 1666 bytes 1667}