⭐️ A friendly language for building type-safe, scalable systems!
at main 33 kB view raw
1mod decision; 2mod expression; 3mod import; 4#[cfg(test)] 5mod tests; 6mod typescript; 7 8use num_bigint::BigInt; 9use num_traits::ToPrimitive; 10 11use crate::analyse::TargetSupport; 12use crate::build::Target; 13use crate::build::package_compiler::StdlibPackage; 14use crate::codegen::TypeScriptDeclarations; 15use crate::type_::PRELUDE_MODULE_NAME; 16use crate::{ 17 ast::{CustomType, Function, Import, ModuleConstant, TypeAlias, *}, 18 docvec, 19 line_numbers::LineNumbers, 20 pretty::*, 21}; 22use camino::Utf8Path; 23use ecow::{EcoString, eco_format}; 24use expression::Context; 25use itertools::Itertools; 26 27use self::import::{Imports, Member}; 28 29const INDENT: isize = 2; 30 31pub const PRELUDE: &str = include_str!("../templates/prelude.mjs"); 32pub const PRELUDE_TS_DEF: &str = include_str!("../templates/prelude.d.mts"); 33 34pub type Output<'a> = Result<Document<'a>, Error>; 35 36#[derive(Debug, Clone, Copy, PartialEq, Eq)] 37pub enum JavaScriptCodegenTarget { 38 JavaScript, 39 TypeScriptDeclarations, 40} 41 42#[derive(Debug)] 43pub struct Generator<'a> { 44 line_numbers: &'a LineNumbers, 45 module: &'a TypedModule, 46 tracker: UsageTracker, 47 module_scope: im::HashMap<EcoString, usize>, 48 current_module_name_segments_count: usize, 49 target_support: TargetSupport, 50 typescript: TypeScriptDeclarations, 51 stdlib_package: StdlibPackage, 52 /// Relative path to the module, surrounded in `"`s to make it a string, and with `\`s escaped 53 /// to `\\`. 54 src_path: EcoString, 55} 56 57impl<'a> Generator<'a> { 58 pub fn new(config: ModuleConfig<'a>) -> Self { 59 let ModuleConfig { 60 target_support, 61 typescript, 62 stdlib_package, 63 module, 64 line_numbers, 65 src: _, 66 path: _, 67 project_root, 68 } = config; 69 let current_module_name_segments_count = module.name.split('/').count(); 70 71 let src_path = &module.type_info.src_path; 72 let src_path = src_path 73 .strip_prefix(project_root) 74 .unwrap_or(src_path) 75 .as_str(); 76 let src_path = eco_format!("\"{src_path}\"").replace("\\", "\\\\"); 77 78 Self { 79 current_module_name_segments_count, 80 line_numbers, 81 module, 82 src_path, 83 tracker: UsageTracker::default(), 84 module_scope: Default::default(), 85 target_support, 86 typescript, 87 stdlib_package, 88 } 89 } 90 91 fn type_reference(&self) -> Document<'a> { 92 if self.typescript == TypeScriptDeclarations::None { 93 return nil(); 94 } 95 96 // Get the name of the module relative the directory (similar to basename) 97 let module = self 98 .module 99 .name 100 .as_str() 101 .split('/') 102 .next_back() 103 .expect("JavaScript generator could not identify imported module name."); 104 105 docvec!["/// <reference types=\"./", module, ".d.mts\" />", line()] 106 } 107 108 pub fn compile(&mut self) -> Output<'a> { 109 // Determine what JavaScript imports we need to generate 110 let mut imports = self.collect_imports(); 111 112 // Determine what names are defined in the module scope so we know to 113 // rename any variables that are defined within functions using the same 114 // names. 115 self.register_module_definitions_in_scope(); 116 117 // Generate JavaScript code for each statement 118 let statements = self.collect_definitions().into_iter().chain( 119 self.module 120 .definitions 121 .iter() 122 .flat_map(|definition| self.definition(definition)), 123 ); 124 125 // Two lines between each statement 126 let mut statements: Vec<_> = 127 Itertools::intersperse(statements, Ok(lines(2))).try_collect()?; 128 129 // Import any prelude functions that have been used 130 131 if self.tracker.ok_used { 132 self.register_prelude_usage(&mut imports, "Ok", None); 133 }; 134 135 if self.tracker.error_used { 136 self.register_prelude_usage(&mut imports, "Error", None); 137 }; 138 139 if self.tracker.list_used { 140 self.register_prelude_usage(&mut imports, "toList", None); 141 }; 142 143 if self.tracker.list_empty_class_used || self.tracker.echo_used { 144 self.register_prelude_usage(&mut imports, "Empty", Some("$Empty")); 145 }; 146 147 if self.tracker.list_non_empty_class_used || self.tracker.echo_used { 148 self.register_prelude_usage(&mut imports, "NonEmpty", Some("$NonEmpty")); 149 }; 150 151 if self.tracker.prepend_used { 152 self.register_prelude_usage(&mut imports, "prepend", Some("listPrepend")); 153 }; 154 155 if self.tracker.custom_type_used || self.tracker.echo_used { 156 self.register_prelude_usage(&mut imports, "CustomType", Some("$CustomType")); 157 }; 158 159 if self.tracker.make_error_used { 160 self.register_prelude_usage(&mut imports, "makeError", None); 161 }; 162 163 if self.tracker.int_remainder_used { 164 self.register_prelude_usage(&mut imports, "remainderInt", None); 165 }; 166 167 if self.tracker.float_division_used { 168 self.register_prelude_usage(&mut imports, "divideFloat", None); 169 }; 170 171 if self.tracker.int_division_used { 172 self.register_prelude_usage(&mut imports, "divideInt", None); 173 }; 174 175 if self.tracker.object_equality_used { 176 self.register_prelude_usage(&mut imports, "isEqual", None); 177 }; 178 179 if self.tracker.bit_array_literal_used { 180 self.register_prelude_usage(&mut imports, "toBitArray", None); 181 } 182 183 if self.tracker.bit_array_slice_used || self.tracker.echo_used { 184 self.register_prelude_usage(&mut imports, "bitArraySlice", None); 185 } 186 187 if self.tracker.bit_array_slice_to_float_used { 188 self.register_prelude_usage(&mut imports, "bitArraySliceToFloat", None); 189 } 190 191 if self.tracker.bit_array_slice_to_int_used || self.tracker.echo_used { 192 self.register_prelude_usage(&mut imports, "bitArraySliceToInt", None); 193 } 194 195 if self.tracker.sized_integer_segment_used { 196 self.register_prelude_usage(&mut imports, "sizedInt", None); 197 } 198 199 if self.tracker.string_bit_array_segment_used { 200 self.register_prelude_usage(&mut imports, "stringBits", None); 201 } 202 203 if self.tracker.string_utf16_bit_array_segment_used { 204 self.register_prelude_usage(&mut imports, "stringToUtf16", None); 205 } 206 207 if self.tracker.string_utf32_bit_array_segment_used { 208 self.register_prelude_usage(&mut imports, "stringToUtf32", None); 209 } 210 211 if self.tracker.codepoint_bit_array_segment_used { 212 self.register_prelude_usage(&mut imports, "codepointBits", None); 213 } 214 215 if self.tracker.codepoint_utf16_bit_array_segment_used { 216 self.register_prelude_usage(&mut imports, "codepointToUtf16", None); 217 } 218 219 if self.tracker.codepoint_utf32_bit_array_segment_used { 220 self.register_prelude_usage(&mut imports, "codepointToUtf32", None); 221 } 222 223 if self.tracker.float_bit_array_segment_used { 224 self.register_prelude_usage(&mut imports, "sizedFloat", None); 225 } 226 227 let echo_definition = self.echo_definition(&mut imports); 228 let type_reference = self.type_reference(); 229 let filepath_definition = self.filepath_definition(); 230 231 // Put it all together 232 233 if imports.is_empty() && statements.is_empty() { 234 Ok(docvec![ 235 type_reference, 236 filepath_definition, 237 "export {}", 238 line(), 239 echo_definition 240 ]) 241 } else if imports.is_empty() { 242 statements.push(line()); 243 Ok(docvec![ 244 type_reference, 245 filepath_definition, 246 statements, 247 echo_definition 248 ]) 249 } else if statements.is_empty() { 250 Ok(docvec![ 251 type_reference, 252 imports.into_doc(JavaScriptCodegenTarget::JavaScript), 253 filepath_definition, 254 echo_definition, 255 ]) 256 } else { 257 Ok(docvec![ 258 type_reference, 259 imports.into_doc(JavaScriptCodegenTarget::JavaScript), 260 line(), 261 filepath_definition, 262 statements, 263 line(), 264 echo_definition 265 ]) 266 } 267 } 268 269 fn echo_definition(&mut self, imports: &mut Imports<'a>) -> Document<'a> { 270 if !self.tracker.echo_used { 271 return nil(); 272 } 273 274 if StdlibPackage::Present == self.stdlib_package { 275 let value = Some(( 276 AssignName::Variable("stdlib$dict".into()), 277 SrcSpan::default(), 278 )); 279 self.register_import(imports, "gleam_stdlib", "dict", &value, &[]); 280 } 281 self.register_prelude_usage(imports, "BitArray", Some("$BitArray")); 282 self.register_prelude_usage(imports, "List", Some("$List")); 283 self.register_prelude_usage(imports, "UtfCodepoint", Some("$UtfCodepoint")); 284 docvec![line(), std::include_str!("../templates/echo.mjs"), line()] 285 } 286 287 fn register_prelude_usage( 288 &self, 289 imports: &mut Imports<'a>, 290 name: &'static str, 291 alias: Option<&'static str>, 292 ) { 293 let path = self.import_path(&self.module.type_info.package, PRELUDE_MODULE_NAME); 294 let member = Member { 295 name: name.to_doc(), 296 alias: alias.map(|a| a.to_doc()), 297 }; 298 imports.register_module(path, [], [member]); 299 } 300 301 pub fn definition(&mut self, definition: &'a TypedDefinition) -> Option<Output<'a>> { 302 match definition { 303 Definition::TypeAlias(TypeAlias { .. }) => None, 304 305 // Handled in collect_imports 306 Definition::Import(Import { .. }) => None, 307 308 // Handled in collect_definitions 309 Definition::CustomType(CustomType { .. }) => None, 310 311 // If a definition is unused then we don't need to generate code for it 312 Definition::ModuleConstant(ModuleConstant { location, .. }) 313 | Definition::Function(Function { location, .. }) 314 if self 315 .module 316 .unused_definition_positions 317 .contains(&location.start) => 318 { 319 None 320 } 321 322 Definition::ModuleConstant(ModuleConstant { 323 publicity, 324 name, 325 value, 326 documentation, 327 .. 328 }) => Some(self.module_constant(*publicity, name, value, documentation)), 329 330 Definition::Function(function) => { 331 // If there's an external JavaScript implementation then it will be imported, 332 // so we don't need to generate a function definition. 333 if function.external_javascript.is_some() { 334 return None; 335 } 336 337 // If the function does not support JavaScript then we don't need to generate 338 // a function definition. 339 if !function.implementations.supports(Target::JavaScript) { 340 return None; 341 } 342 343 self.module_function(function) 344 } 345 } 346 } 347 348 fn custom_type_definition( 349 &mut self, 350 constructors: &'a [TypedRecordConstructor], 351 publicity: Publicity, 352 opaque: bool, 353 ) -> Vec<Output<'a>> { 354 // If there's no constructors then there's nothing to do here. 355 if constructors.is_empty() { 356 return vec![]; 357 } 358 359 self.tracker.custom_type_used = true; 360 constructors 361 .iter() 362 .map(|constructor| Ok(self.record_definition(constructor, publicity, opaque))) 363 .collect() 364 } 365 366 fn record_definition( 367 &self, 368 constructor: &'a TypedRecordConstructor, 369 publicity: Publicity, 370 opaque: bool, 371 ) -> Document<'a> { 372 fn parameter((i, arg): (usize, &TypedRecordConstructorArg)) -> Document<'_> { 373 arg.label 374 .as_ref() 375 .map(|(_, s)| maybe_escape_identifier(s)) 376 .unwrap_or_else(|| eco_format!("${i}")) 377 .to_doc() 378 } 379 380 let doc = if let Some((_, documentation)) = &constructor.documentation { 381 jsdoc_comment(documentation, publicity).append(line()) 382 } else { 383 nil() 384 }; 385 386 let head = if publicity.is_private() || opaque { 387 "class " 388 } else { 389 "export class " 390 }; 391 let head = docvec![head, &constructor.name, " extends $CustomType {"]; 392 393 if constructor.arguments.is_empty() { 394 return head.append("}"); 395 }; 396 397 let parameters = join( 398 constructor.arguments.iter().enumerate().map(parameter), 399 break_(",", ", "), 400 ); 401 402 let constructor_body = join( 403 constructor.arguments.iter().enumerate().map(|(i, arg)| { 404 let var = parameter((i, arg)); 405 match &arg.label { 406 None => docvec!["this[", i, "] = ", var, ";"], 407 Some((_, name)) => { 408 docvec!["this.", maybe_escape_property(name), " = ", var, ";"] 409 } 410 } 411 }), 412 line(), 413 ); 414 415 let class_body = docvec![ 416 line(), 417 "constructor(", 418 parameters, 419 ") {", 420 docvec![line(), "super();", line(), constructor_body].nest(INDENT), 421 line(), 422 "}", 423 ] 424 .nest(INDENT); 425 426 docvec![doc, head, class_body, line(), "}"] 427 } 428 429 fn collect_definitions(&mut self) -> Vec<Output<'a>> { 430 self.module 431 .definitions 432 .iter() 433 .flat_map(|definition| match definition { 434 // If a custom type is unused then we don't need to generate code for it 435 Definition::CustomType(CustomType { location, .. }) 436 if self 437 .module 438 .unused_definition_positions 439 .contains(&location.start) => 440 { 441 vec![] 442 } 443 444 Definition::CustomType(CustomType { 445 publicity, 446 constructors, 447 opaque, 448 .. 449 }) => self.custom_type_definition(constructors, *publicity, *opaque), 450 451 Definition::Function(Function { .. }) 452 | Definition::TypeAlias(TypeAlias { .. }) 453 | Definition::Import(Import { .. }) 454 | Definition::ModuleConstant(ModuleConstant { .. }) => vec![], 455 }) 456 .collect() 457 } 458 459 fn collect_imports(&mut self) -> Imports<'a> { 460 let mut imports = Imports::new(); 461 462 for definition in &self.module.definitions { 463 match definition { 464 Definition::Import(Import { 465 module, 466 as_name, 467 unqualified_values: unqualified, 468 package, 469 .. 470 }) => { 471 self.register_import(&mut imports, package, module, as_name, unqualified); 472 } 473 474 Definition::Function(Function { 475 name: Some((_, name)), 476 publicity, 477 external_javascript: Some((module, function, _location)), 478 .. 479 }) => { 480 self.register_external_function( 481 &mut imports, 482 *publicity, 483 name, 484 module, 485 function, 486 ); 487 } 488 489 Definition::Function(Function { .. }) 490 | Definition::TypeAlias(TypeAlias { .. }) 491 | Definition::CustomType(CustomType { .. }) 492 | Definition::ModuleConstant(ModuleConstant { .. }) => (), 493 } 494 } 495 496 imports 497 } 498 499 fn import_path(&self, package: &'a str, module: &'a str) -> EcoString { 500 // TODO: strip shared prefixed between current module and imported 501 // module to avoid descending and climbing back out again 502 if package == self.module.type_info.package || package.is_empty() { 503 // Same package 504 match self.current_module_name_segments_count { 505 1 => eco_format!("./{module}.mjs"), 506 _ => { 507 let prefix = "../".repeat(self.current_module_name_segments_count - 1); 508 eco_format!("{prefix}{module}.mjs") 509 } 510 } 511 } else { 512 // Different package 513 let prefix = "../".repeat(self.current_module_name_segments_count); 514 eco_format!("{prefix}{package}/{module}.mjs") 515 } 516 } 517 518 fn register_import( 519 &mut self, 520 imports: &mut Imports<'a>, 521 package: &'a str, 522 module: &'a str, 523 as_name: &Option<(AssignName, SrcSpan)>, 524 unqualified: &[UnqualifiedImport], 525 ) { 526 let get_name = |module: &'a str| { 527 module 528 .split('/') 529 .next_back() 530 .expect("JavaScript generator could not identify imported module name.") 531 }; 532 533 let (discarded, module_name) = match as_name { 534 None => (false, get_name(module)), 535 Some((AssignName::Discard(_), _)) => (true, get_name(module)), 536 Some((AssignName::Variable(name), _)) => (false, name.as_str()), 537 }; 538 539 let module_name = eco_format!("${module_name}"); 540 let path = self.import_path(package, module); 541 let unqualified_imports = unqualified.iter().map(|i| { 542 let alias = i.as_name.as_ref().map(|n| { 543 self.register_in_scope(n); 544 maybe_escape_identifier(n).to_doc() 545 }); 546 let name = maybe_escape_identifier(&i.name).to_doc(); 547 Member { name, alias } 548 }); 549 550 let aliases = if discarded { vec![] } else { vec![module_name] }; 551 imports.register_module(path, aliases, unqualified_imports); 552 } 553 554 fn register_external_function( 555 &mut self, 556 imports: &mut Imports<'a>, 557 publicity: Publicity, 558 name: &'a str, 559 module: &'a str, 560 fun: &'a str, 561 ) { 562 let needs_escaping = !is_usable_js_identifier(name); 563 let member = Member { 564 name: fun.to_doc(), 565 alias: if name == fun && !needs_escaping { 566 None 567 } else if needs_escaping { 568 Some(escape_identifier(name).to_doc()) 569 } else { 570 Some(name.to_doc()) 571 }, 572 }; 573 if publicity.is_importable() { 574 imports.register_export(maybe_escape_identifier_string(name)) 575 } 576 imports.register_module(EcoString::from(module), [], [member]); 577 } 578 579 fn module_constant( 580 &mut self, 581 publicity: Publicity, 582 name: &'a EcoString, 583 value: &'a TypedConstant, 584 documentation: &'a Option<(u32, EcoString)>, 585 ) -> Output<'a> { 586 let head = if publicity.is_private() { 587 "const " 588 } else { 589 "export const " 590 }; 591 592 let mut generator = expression::Generator::new( 593 self.module.name.clone(), 594 self.src_path.clone(), 595 self.line_numbers, 596 "".into(), 597 vec![], 598 &mut self.tracker, 599 self.module_scope.clone(), 600 ); 601 602 let document = generator.constant_expression(Context::Constant, value)?; 603 604 let jsdoc = if let Some((_, documentation)) = documentation { 605 jsdoc_comment(documentation, publicity).append(line()) 606 } else { 607 nil() 608 }; 609 610 Ok(docvec![ 611 jsdoc, 612 head, 613 maybe_escape_identifier(name), 614 " = ", 615 document, 616 ";", 617 ]) 618 } 619 620 fn register_in_scope(&mut self, name: &str) { 621 let _ = self.module_scope.insert(name.into(), 0); 622 } 623 624 fn module_function(&mut self, function: &'a TypedFunction) -> Option<Output<'a>> { 625 let (_, name) = function 626 .name 627 .as_ref() 628 .expect("A module's function must be named"); 629 let argument_names = function 630 .arguments 631 .iter() 632 .map(|arg| arg.names.get_variable_name()) 633 .collect(); 634 let mut generator = expression::Generator::new( 635 self.module.name.clone(), 636 self.src_path.clone(), 637 self.line_numbers, 638 name.clone(), 639 argument_names, 640 &mut self.tracker, 641 self.module_scope.clone(), 642 ); 643 644 let function_doc = match &function.documentation { 645 None => nil(), 646 Some((_, documentation)) => { 647 jsdoc_comment(documentation, function.publicity).append(line()) 648 } 649 }; 650 651 let head = if function.publicity.is_private() { 652 "function " 653 } else { 654 "export function " 655 }; 656 657 let body = match generator.function_body(&function.body, function.arguments.as_slice()) { 658 // No error, let's continue! 659 Ok(body) => body, 660 661 // There is an error coming from some expression that is not supported on JavaScript 662 // and the target support is not enforced. In this case we do not error, instead 663 // returning nothing which will cause no function to be generated. 664 Err(error) if error.is_unsupported() && !self.target_support.is_enforced() => { 665 return None; 666 } 667 668 // Some other error case which will be returned to the user. 669 Err(error) => return Some(Err(error)), 670 }; 671 672 let document = docvec![ 673 function_doc, 674 head, 675 maybe_escape_identifier(name.as_str()), 676 fun_args(function.arguments.as_slice(), generator.tail_recursion_used), 677 " {", 678 docvec![line(), body].nest(INDENT).group(), 679 line(), 680 "}", 681 ]; 682 Some(Ok(document)) 683 } 684 685 fn register_module_definitions_in_scope(&mut self) { 686 for definition in self.module.definitions.iter() { 687 match definition { 688 Definition::ModuleConstant(ModuleConstant { name, .. }) => { 689 self.register_in_scope(name) 690 } 691 692 Definition::Function(Function { name, .. }) => self.register_in_scope( 693 name.as_ref() 694 .map(|(_, name)| name) 695 .expect("Function in a definition must be named"), 696 ), 697 698 Definition::Import(Import { 699 unqualified_values: unqualified, 700 .. 701 }) => unqualified 702 .iter() 703 .for_each(|unq_import| self.register_in_scope(unq_import.used_name())), 704 705 Definition::TypeAlias(TypeAlias { .. }) 706 | Definition::CustomType(CustomType { .. }) => (), 707 } 708 } 709 } 710 711 fn filepath_definition(&self) -> Document<'a> { 712 if !self.tracker.make_error_used { 713 return nil(); 714 } 715 716 docvec!["const FILEPATH = ", self.src_path.clone(), ';', lines(2)] 717 } 718} 719 720fn jsdoc_comment(documentation: &EcoString, publicity: Publicity) -> Document<'_> { 721 let doc_lines = documentation 722 .trim_end() 723 .split('\n') 724 .map(|line| eco_format!(" *{line}").to_doc()) 725 .collect_vec(); 726 727 // We start with the documentation of the function 728 let doc_body = join(doc_lines, line()); 729 let mut doc = docvec!["/**", line(), doc_body, line()]; 730 if !publicity.is_public() { 731 // If the function is not public we hide the documentation using 732 // the `@ignore` tag: https://jsdoc.app/tags-ignore 733 doc = docvec![doc, " * ", line(), " * @ignore", line()]; 734 } 735 // And finally we close the doc comment 736 docvec![doc, " */"] 737} 738 739#[derive(Debug)] 740pub struct ModuleConfig<'a> { 741 pub module: &'a TypedModule, 742 pub line_numbers: &'a LineNumbers, 743 pub src: &'a EcoString, 744 pub target_support: TargetSupport, 745 pub typescript: TypeScriptDeclarations, 746 pub stdlib_package: StdlibPackage, 747 pub path: &'a Utf8Path, 748 pub project_root: &'a Utf8Path, 749} 750 751pub fn module(config: ModuleConfig<'_>) -> Result<String, crate::Error> { 752 let path = config.path.to_path_buf(); 753 let src = config.src.clone(); 754 let document = Generator::new(config) 755 .compile() 756 .map_err(|error| crate::Error::JavaScript { path, src, error })?; 757 Ok(document.to_pretty_string(80)) 758} 759 760pub fn ts_declaration( 761 module: &TypedModule, 762 path: &Utf8Path, 763 src: &EcoString, 764) -> Result<String, crate::Error> { 765 let document = typescript::TypeScriptGenerator::new(module) 766 .compile() 767 .map_err(|error| crate::Error::JavaScript { 768 path: path.to_path_buf(), 769 src: src.clone(), 770 error, 771 })?; 772 Ok(document.to_pretty_string(80)) 773} 774 775#[derive(Debug, Clone, PartialEq, Eq)] 776pub enum Error { 777 Unsupported { feature: String, location: SrcSpan }, 778} 779 780impl Error { 781 /// Returns `true` if the error is [`Unsupported`]. 782 /// 783 /// [`Unsupported`]: Error::Unsupported 784 #[must_use] 785 pub fn is_unsupported(&self) -> bool { 786 matches!(self, Self::Unsupported { .. }) 787 } 788} 789 790fn fun_args(args: &'_ [TypedArg], tail_recursion_used: bool) -> Document<'_> { 791 let mut discards = 0; 792 wrap_args(args.iter().map(|a| match a.get_variable_name() { 793 None => { 794 let doc = if discards == 0 { 795 "_".to_doc() 796 } else { 797 eco_format!("_{discards}").to_doc() 798 }; 799 discards += 1; 800 doc 801 } 802 Some(name) if tail_recursion_used => eco_format!("loop${name}").to_doc(), 803 Some(name) => maybe_escape_identifier(name).to_doc(), 804 })) 805} 806 807fn wrap_args<'a, I>(args: I) -> Document<'a> 808where 809 I: IntoIterator<Item = Document<'a>>, 810{ 811 break_("", "") 812 .append(join(args, break_(",", ", "))) 813 .nest(INDENT) 814 .append(break_("", "")) 815 .surround("(", ")") 816 .group() 817} 818 819fn wrap_object<'a>( 820 items: impl IntoIterator<Item = (Document<'a>, Option<Document<'a>>)>, 821) -> Document<'a> { 822 let mut empty = true; 823 let fields = items.into_iter().map(|(key, value)| { 824 empty = false; 825 match value { 826 Some(value) => docvec![key, ": ", value], 827 None => key.to_doc(), 828 } 829 }); 830 let fields = join(fields, break_(",", ", ")); 831 832 if empty { 833 "{}".to_doc() 834 } else { 835 docvec![ 836 docvec!["{", break_("", " "), fields] 837 .nest(INDENT) 838 .append(break_("", " ")) 839 .group(), 840 "}" 841 ] 842 } 843} 844 845fn is_usable_js_identifier(word: &str) -> bool { 846 !matches!( 847 word, 848 // Keywords and reserved words 849 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar 850 "await" 851 | "arguments" 852 | "break" 853 | "case" 854 | "catch" 855 | "class" 856 | "const" 857 | "continue" 858 | "debugger" 859 | "default" 860 | "delete" 861 | "do" 862 | "else" 863 | "enum" 864 | "export" 865 | "extends" 866 | "eval" 867 | "false" 868 | "finally" 869 | "for" 870 | "function" 871 | "if" 872 | "implements" 873 | "import" 874 | "in" 875 | "instanceof" 876 | "interface" 877 | "let" 878 | "new" 879 | "null" 880 | "package" 881 | "private" 882 | "protected" 883 | "public" 884 | "return" 885 | "static" 886 | "super" 887 | "switch" 888 | "this" 889 | "throw" 890 | "true" 891 | "try" 892 | "typeof" 893 | "var" 894 | "void" 895 | "while" 896 | "with" 897 | "yield" 898 // `undefined` to avoid any unintentional overriding. 899 | "undefined" 900 // `then` to avoid a module that defines a `then` function being 901 // used as a `thenable` in JavaScript when the module is imported 902 // dynamically, which results in unexpected behaviour. 903 // It is rather unfortunate that we have to do this. 904 | "then" 905 ) 906} 907 908fn is_usable_js_property(label: &str) -> bool { 909 match label { 910 // `then` to avoid a custom type that defines a `then` function being 911 // used as a `thenable` in Javascript. 912 "then" 913 // `constructor` to avoid unintentional overriding of the constructor of 914 // records, leading to potential runtime crashes while using `withFields`. 915 | "constructor" 916 // `prototype` and `__proto__` to avoid unintentionally overriding the 917 // prototype chain. 918 | "prototpye" | "__proto__" => false, 919 _ => true 920 } 921} 922 923fn maybe_escape_identifier_string(word: &str) -> EcoString { 924 if is_usable_js_identifier(word) { 925 EcoString::from(word) 926 } else { 927 escape_identifier(word) 928 } 929} 930 931fn escape_identifier(word: &str) -> EcoString { 932 eco_format!("{word}$") 933} 934 935fn maybe_escape_identifier(word: &str) -> EcoString { 936 if is_usable_js_identifier(word) { 937 EcoString::from(word) 938 } else { 939 escape_identifier(word) 940 } 941} 942 943fn maybe_escape_property(label: &str) -> EcoString { 944 if is_usable_js_property(label) { 945 EcoString::from(label) 946 } else { 947 escape_identifier(label) 948 } 949} 950 951#[derive(Debug, Default)] 952pub(crate) struct UsageTracker { 953 pub ok_used: bool, 954 pub list_used: bool, 955 pub list_empty_class_used: bool, 956 pub list_non_empty_class_used: bool, 957 pub prepend_used: bool, 958 pub error_used: bool, 959 pub int_remainder_used: bool, 960 pub make_error_used: bool, 961 pub custom_type_used: bool, 962 pub int_division_used: bool, 963 pub float_division_used: bool, 964 pub object_equality_used: bool, 965 pub bit_array_literal_used: bool, 966 pub bit_array_slice_used: bool, 967 pub bit_array_slice_to_float_used: bool, 968 pub bit_array_slice_to_int_used: bool, 969 pub sized_integer_segment_used: bool, 970 pub string_bit_array_segment_used: bool, 971 pub string_utf16_bit_array_segment_used: bool, 972 pub string_utf32_bit_array_segment_used: bool, 973 pub codepoint_bit_array_segment_used: bool, 974 pub codepoint_utf16_bit_array_segment_used: bool, 975 pub codepoint_utf32_bit_array_segment_used: bool, 976 pub float_bit_array_segment_used: bool, 977 pub echo_used: bool, 978} 979 980fn bool(bool: bool) -> Document<'static> { 981 match bool { 982 true => "true".to_doc(), 983 false => "false".to_doc(), 984 } 985} 986 987/// Int segments <= 48 bits wide in bit arrays are within JavaScript's safe range and are evaluated 988/// at compile time when all inputs are known. This is done for both bit array expressions and 989/// pattern matching. 990/// 991/// Int segments of any size could be evaluated at compile time, but currently aren't due to the 992/// potential for causing large generated JS for inputs such as `<<0:8192>>`. 993/// 994pub(crate) const SAFE_INT_SEGMENT_MAX_SIZE: usize = 48; 995 996/// Evaluates the value of an Int segment in a bit array into its corresponding bytes. This avoids 997/// needing to do the evaluation at runtime when all inputs are known at compile-time. 998/// 999pub(crate) fn bit_array_segment_int_value_to_bytes( 1000 mut value: BigInt, 1001 size: BigInt, 1002 endianness: Endianness, 1003) -> Result<Vec<u8>, Error> { 1004 // Clamp negative sizes to zero 1005 let size = size.max(BigInt::ZERO); 1006 1007 // Convert size to u32. This is safe because this function isn't called with a size greater 1008 // than `SAFE_INT_SEGMENT_MAX_SIZE`. 1009 let size = size 1010 .to_u32() 1011 .expect("bit array segment size to be a valid u32"); 1012 1013 // Convert negative number to two's complement representation 1014 if value < BigInt::ZERO { 1015 let value_modulus = BigInt::from(2).pow(size); 1016 value = &value_modulus + (value % &value_modulus); 1017 } 1018 1019 // Convert value to the desired number of bytes 1020 let mut bytes = vec![0u8; size as usize / 8]; 1021 for byte in bytes.iter_mut() { 1022 *byte = (&value % BigInt::from(256)) 1023 .to_u8() 1024 .expect("modulo result to be a valid u32"); 1025 value /= BigInt::from(256); 1026 } 1027 1028 if endianness.is_big() { 1029 bytes.reverse(); 1030 } 1031 1032 Ok(bytes) 1033}