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
18 kB 618 lines
1mod expression; 2mod import; 3mod pattern; 4#[cfg(test)] 5mod tests; 6 7use std::path::Path; 8 9use crate::{ast::*, docvec, io::Utf8Writer, line_numbers::LineNumbers, pretty::*}; 10use itertools::Itertools; 11 12use self::import::{Imports, Member}; 13 14const INDENT: isize = 2; 15 16pub const PRELUDE: &str = include_str!("../templates/prelude.js"); 17 18pub type Output<'a> = Result<Document<'a>, Error>; 19 20#[derive(Debug)] 21pub struct Generator<'a> { 22 line_numbers: &'a LineNumbers, 23 module: &'a TypedModule, 24 tracker: UsageTracker, 25 module_scope: im::HashMap<String, usize>, 26} 27 28impl<'a> Generator<'a> { 29 pub fn new(line_numbers: &'a LineNumbers, module: &'a TypedModule) -> Self { 30 Self { 31 line_numbers, 32 module, 33 tracker: UsageTracker::default(), 34 module_scope: Default::default(), 35 } 36 } 37 38 pub fn compile(&mut self) -> Output<'a> { 39 let mut imports = self.collect_imports(); 40 let statements = self 41 .module 42 .statements 43 .iter() 44 .flat_map(|s| self.statement(s)); 45 46 // Two lines between each statement 47 let mut statements: Vec<_> = 48 Itertools::intersperse(statements, Ok(lines(2))).try_collect()?; 49 50 // Import any prelude functions that have been used 51 52 if self.tracker.ok_used { 53 self.register_prelude_usage(&mut imports, "Ok"); 54 }; 55 56 if self.tracker.error_used { 57 self.register_prelude_usage(&mut imports, "Error"); 58 }; 59 60 if self.tracker.list_used { 61 self.register_prelude_usage(&mut imports, "toList"); 62 }; 63 64 if self.tracker.custom_type_used { 65 self.register_prelude_usage(&mut imports, "CustomType"); 66 }; 67 68 if self.tracker.throw_error_used { 69 self.register_prelude_usage(&mut imports, "throwError"); 70 }; 71 72 if self.tracker.float_division_used { 73 self.register_prelude_usage(&mut imports, "divideFloat"); 74 }; 75 76 if self.tracker.int_division_used { 77 self.register_prelude_usage(&mut imports, "divideInt"); 78 }; 79 80 if self.tracker.object_equality_used { 81 self.register_prelude_usage(&mut imports, "isEqual"); 82 }; 83 84 if self.tracker.bit_string_literal_used { 85 self.register_prelude_usage(&mut imports, "toBitString"); 86 }; 87 88 if self.tracker.string_bit_string_segment_used { 89 self.register_prelude_usage(&mut imports, "stringBits"); 90 }; 91 92 if self.tracker.codepoint_bit_string_segment_used { 93 self.register_prelude_usage(&mut imports, "codepointBits"); 94 }; 95 96 // Put it all together 97 98 if imports.is_empty() && statements.is_empty() { 99 Ok(docvec!("export {}", line())) 100 } else if imports.is_empty() { 101 statements.push(line()); 102 Ok(statements.to_doc()) 103 } else if statements.is_empty() { 104 Ok(imports.into_doc()) 105 } else { 106 Ok(docvec![imports.into_doc(), line(), statements, line()]) 107 } 108 } 109 110 fn register_prelude_usage(&self, imports: &mut Imports<'a>, name: &'static str) { 111 let path = self.import_path(&self.module.type_info.package, &["gleam".to_string()]); 112 let member = Member { 113 name: name.to_doc(), 114 alias: None, 115 }; 116 imports.register_module(path, [], [member]); 117 } 118 119 pub fn statement(&mut self, statement: &'a TypedStatement) -> Vec<Output<'a>> { 120 match statement { 121 Statement::TypeAlias { .. } | Statement::ExternalType { .. } => vec![], 122 123 Statement::Import { .. } => vec![], 124 125 Statement::CustomType { 126 public, 127 constructors, 128 opaque, 129 .. 130 } => self.custom_type_definition(constructors, *public, *opaque), 131 132 Statement::ModuleConstant { 133 public, 134 name, 135 value, 136 .. 137 } => vec![self.module_constant(*public, name, value)], 138 139 Statement::Fn { 140 arguments, 141 name, 142 body, 143 public, 144 .. 145 } => vec![self.module_function(*public, name, arguments, body)], 146 147 Statement::ExternalFn { 148 public, 149 name, 150 arguments, 151 module, 152 fun, 153 .. 154 } if module.is_empty() => vec![Ok( 155 self.global_external_function(*public, name, arguments, fun) 156 )], 157 158 Statement::ExternalFn { .. } => vec![], 159 } 160 } 161 162 fn custom_type_definition( 163 &mut self, 164 constructors: &'a [TypedRecordConstructor], 165 public: bool, 166 opaque: bool, 167 ) -> Vec<Output<'a>> { 168 self.tracker.custom_type_used = true; 169 constructors 170 .iter() 171 .map(|constructor| Ok(self.record_definition(constructor, public, opaque))) 172 .collect() 173 } 174 175 fn record_definition( 176 &self, 177 constructor: &'a TypedRecordConstructor, 178 public: bool, 179 opaque: bool, 180 ) -> Document<'a> { 181 fn parameter((i, arg): (usize, &TypedRecordConstructorArg)) -> Document<'_> { 182 arg.label 183 .as_ref() 184 .map(|s| maybe_escape_identifier_doc(s)) 185 .unwrap_or_else(|| Document::String(format!("x{}", i))) 186 } 187 188 let head = if public && !opaque { 189 "export class " 190 } else { 191 "class " 192 }; 193 let head = docvec![head, &constructor.name, " extends CustomType {"]; 194 195 if constructor.arguments.is_empty() { 196 return head.append("}"); 197 }; 198 199 let parameters = concat(Itertools::intersperse( 200 constructor.arguments.iter().enumerate().map(parameter), 201 break_(",", ", "), 202 )); 203 204 let constructor_body = concat(Itertools::intersperse( 205 constructor.arguments.iter().enumerate().map(|(i, arg)| { 206 let var = parameter((i, arg)); 207 match &arg.label { 208 None => docvec!["this[", i, "] = ", var, ";"], 209 Some(name) => docvec!["this.", name, " = ", var, ";"], 210 } 211 }), 212 line(), 213 )); 214 215 let class_body = docvec![ 216 line(), 217 "constructor(", 218 parameters, 219 ") {", 220 docvec![line(), "super();", line(), constructor_body].nest(INDENT), 221 line(), 222 "}", 223 ] 224 .nest(INDENT); 225 226 docvec![head, class_body, line(), "}"] 227 } 228 229 fn collect_imports(&mut self) -> Imports<'a> { 230 let mut imports = Imports::new(); 231 232 for statement in &self.module.statements { 233 match statement { 234 Statement::Fn { .. } 235 | Statement::TypeAlias { .. } 236 | Statement::CustomType { .. } 237 | Statement::ExternalType { .. } 238 | Statement::ModuleConstant { .. } => (), 239 Statement::ExternalFn { module, .. } if module.is_empty() => (), 240 241 Statement::ExternalFn { 242 public, 243 name, 244 module, 245 fun, 246 .. 247 } => self.register_external_function(&mut imports, *public, name, module, fun), 248 249 Statement::Import { 250 module, 251 as_name, 252 unqualified, 253 package, 254 .. 255 } => { 256 self.register_import(&mut imports, package, module, as_name, unqualified); 257 } 258 } 259 } 260 261 imports 262 } 263 264 fn import_path(&self, package: &'a str, module: &'a [String]) -> String { 265 let path = module.join("/"); 266 267 if package == self.module.type_info.package || package.is_empty() { 268 // Same package uses relative paths 269 // TODO: strip shared prefixed between current module and imported 270 // module to avoid decending and climbing back out again 271 match self.module.name.len() { 272 1 => format!("./{}.mjs", path), 273 _ => { 274 let prefix = "../".repeat(self.module.name.len() - 1); 275 format!("{}{}.mjs", prefix, path) 276 } 277 } 278 } else { 279 // Different packages uses absolute imports 280 format!("gleam-packages/{}/{}.mjs", package, path) 281 } 282 } 283 284 fn register_import( 285 &mut self, 286 imports: &mut Imports<'a>, 287 package: &'a str, 288 module: &'a [String], 289 as_name: &'a Option<String>, 290 unqualified: &'a [UnqualifiedImport], 291 ) { 292 let module_name = as_name.as_ref().unwrap_or_else(|| { 293 module 294 .last() 295 .expect("JavaScript generator could not identify imported module name.") 296 }); 297 let module_name = format!("${}", module_name); 298 let path = self.import_path(package, module); 299 let unqualified_imports = unqualified 300 .iter() 301 // We do not create a JS import for types as they are not used at runtime 302 .filter(|import| import.is_value()) 303 .map(|i| { 304 let alias = i.as_name.as_ref().map(|n| { 305 self.register_in_scope(n); 306 maybe_escape_identifier_doc(n) 307 }); 308 let name = maybe_escape_identifier_doc(&i.name); 309 Member { name, alias } 310 }); 311 imports.register_module(path, [module_name], unqualified_imports); 312 } 313 314 fn register_external_function( 315 &mut self, 316 imports: &mut Imports<'a>, 317 public: bool, 318 name: &'a str, 319 module: &'a str, 320 fun: &'a str, 321 ) { 322 let member = Member { 323 name: fun.to_doc(), 324 alias: if name == fun { 325 None 326 } else { 327 Some(maybe_escape_identifier_doc(name)) 328 }, 329 }; 330 if public { 331 imports.register_export(maybe_escape_identifier_string(name)) 332 } 333 imports.register_module(module.to_string(), [], [member]); 334 } 335 336 fn module_constant( 337 &mut self, 338 public: bool, 339 name: &'a str, 340 value: &'a TypedConstant, 341 ) -> Output<'a> { 342 let head = if public { "export const " } else { "const " }; 343 self.register_in_scope(name); 344 Ok(docvec![ 345 head, 346 maybe_escape_identifier_doc(name), 347 " = ", 348 expression::constant_expression(&mut self.tracker, value)?, 349 ";", 350 ]) 351 } 352 353 fn register_in_scope(&mut self, name: &str) { 354 let _ = self.module_scope.insert(name.to_string(), 0); 355 } 356 357 fn module_function( 358 &mut self, 359 public: bool, 360 name: &'a str, 361 args: &'a [TypedArg], 362 body: &'a TypedExpr, 363 ) -> Output<'a> { 364 self.register_in_scope(name); 365 let argument_names = args 366 .iter() 367 .map(|arg| arg.names.get_variable_name()) 368 .collect(); 369 let mut generator = expression::Generator::new( 370 &self.module.name, 371 self.line_numbers, 372 name, 373 argument_names, 374 &mut self.tracker, 375 self.module_scope.clone(), 376 ); 377 let head = if public { 378 "export function " 379 } else { 380 "function " 381 }; 382 let body = generator.function_body(body, args)?; 383 Ok(docvec![ 384 head, 385 maybe_escape_identifier_doc(name), 386 fun_args(args, generator.tail_recursion_used), 387 " {", 388 docvec![line(), body].nest(INDENT).group(), 389 line(), 390 "}", 391 ]) 392 } 393 394 fn global_external_function<T>( 395 &mut self, 396 public: bool, 397 name: &'a str, 398 arguments: &'a [ExternalFnArg<T>], 399 fun: &'a str, 400 ) -> Document<'a> { 401 let head = if public { 402 "export function " 403 } else { 404 "function " 405 }; 406 let args = external_fn_args(arguments); 407 let fun = if name == fun { 408 docvec!["globalThis.", fun] 409 } else { 410 fun.to_doc() 411 }; 412 let body = docvec!["return ", fun, args.clone()]; 413 let body = docvec![line(), body].nest(INDENT).group(); 414 docvec![head, name, args, " {", body, line(), "}"] 415 } 416} 417 418fn external_fn_args<T>(arguments: &[ExternalFnArg<T>]) -> Document<'_> { 419 wrap_args( 420 arguments 421 .iter() 422 .enumerate() 423 .map(|(index, ExternalFnArg { label, .. })| { 424 label 425 .as_ref() 426 .map(|l| l.to_doc()) 427 .unwrap_or_else(|| Document::String(format!("arg{}", index))) 428 }), 429 ) 430} 431 432pub fn module( 433 module: &TypedModule, 434 line_numbers: &LineNumbers, 435 path: &Path, 436 src: &str, 437 writer: &mut impl Utf8Writer, 438) -> Result<(), crate::Error> { 439 Generator::new(line_numbers, module) 440 .compile() 441 .map_err(|error| crate::Error::JavaScript { 442 path: path.to_path_buf(), 443 src: src.to_string(), 444 error, 445 })? 446 .pretty_print(80, writer) 447} 448 449#[derive(Debug, Clone, PartialEq)] 450pub enum Error { 451 Unsupported { feature: String, location: SrcSpan }, 452} 453 454fn fun_args(args: &'_ [TypedArg], tail_recursion_used: bool) -> Document<'_> { 455 let mut discards = 0; 456 wrap_args(args.iter().map(|a| match a.get_variable_name() { 457 None => { 458 let doc = if discards == 0 { 459 "_".to_doc() 460 } else { 461 Document::String(format!("_{}", discards)) 462 }; 463 discards += 1; 464 doc 465 } 466 Some(name) if tail_recursion_used => Document::String(format!("loop${}", name)), 467 Some(name) => maybe_escape_identifier_doc(name), 468 })) 469} 470 471fn wrap_args<'a, I>(args: I) -> Document<'a> 472where 473 I: IntoIterator<Item = Document<'a>>, 474{ 475 break_("", "") 476 .append(concat(Itertools::intersperse( 477 args.into_iter(), 478 break_(",", ", "), 479 ))) 480 .nest(INDENT) 481 .append(break_("", "")) 482 .surround("(", ")") 483 .group() 484} 485 486fn wrap_object<'a>( 487 items: impl IntoIterator<Item = (Document<'a>, Option<Document<'a>>)>, 488) -> Document<'a> { 489 let mut empty = true; 490 let fields = items.into_iter().map(|(key, value)| { 491 empty = false; 492 match value { 493 Some(value) => docvec![key, ": ", value], 494 None => key.to_doc(), 495 } 496 }); 497 let fields = concat(Itertools::intersperse(fields, break_(",", ", "))); 498 499 if empty { 500 "{}".to_doc() 501 } else { 502 docvec![ 503 docvec!["{", break_("", " "), fields] 504 .nest(INDENT) 505 .append(break_("", " ")) 506 .group(), 507 "}" 508 ] 509 } 510} 511 512fn try_wrap_object<'a>(items: impl IntoIterator<Item = (Document<'a>, Output<'a>)>) -> Output<'a> { 513 let fields = items 514 .into_iter() 515 .map(|(key, value)| Ok(docvec![key, ": ", value?])); 516 let fields: Vec<_> = Itertools::intersperse(fields, Ok(break_(",", ", "))).try_collect()?; 517 518 Ok(docvec![ 519 docvec!["{", break_("", " "), fields] 520 .nest(INDENT) 521 .append(break_("", " ")) 522 .group(), 523 "}" 524 ]) 525} 526 527// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar 528// And we add `undefined` to avoid any unintentional overriding which could 529// cause bugs. 530fn is_valid_js_identifier(word: &str) -> bool { 531 !matches!( 532 word, 533 "await" 534 | "arguments" 535 | "break" 536 | "case" 537 | "catch" 538 | "class" 539 | "const" 540 | "continue" 541 | "debugger" 542 | "default" 543 | "delete" 544 | "do" 545 | "else" 546 | "enum" 547 | "export" 548 | "extends" 549 | "eval" 550 | "false" 551 | "finally" 552 | "for" 553 | "function" 554 | "if" 555 | "implements" 556 | "import" 557 | "in" 558 | "instanceof" 559 | "interface" 560 | "let" 561 | "new" 562 | "null" 563 | "package" 564 | "private" 565 | "protected" 566 | "public" 567 | "return" 568 | "static" 569 | "super" 570 | "switch" 571 | "this" 572 | "throw" 573 | "true" 574 | "try" 575 | "typeof" 576 | "undefined" 577 | "var" 578 | "void" 579 | "while" 580 | "with" 581 | "yield" 582 ) 583} 584 585fn maybe_escape_identifier_string(word: &str) -> String { 586 if is_valid_js_identifier(word) { 587 word.to_string() 588 } else { 589 escape_identifier(word) 590 } 591} 592 593fn escape_identifier(word: &str) -> String { 594 format!("{}$", word) 595} 596 597fn maybe_escape_identifier_doc(word: &str) -> Document<'_> { 598 if is_valid_js_identifier(word) { 599 word.to_doc() 600 } else { 601 Document::String(escape_identifier(word)) 602 } 603} 604 605#[derive(Debug, Default)] 606pub(crate) struct UsageTracker { 607 pub ok_used: bool, 608 pub list_used: bool, 609 pub error_used: bool, 610 pub throw_error_used: bool, 611 pub custom_type_used: bool, 612 pub int_division_used: bool, 613 pub float_division_used: bool, 614 pub object_equality_used: bool, 615 pub bit_string_literal_used: bool, 616 pub string_bit_string_segment_used: bool, 617 pub codepoint_bit_string_segment_used: bool, 618}