Fork of daniellemaywood.uk/gleam — Wasm codegen work
Something went wrong. Try again.
56 kB · 1667 lines
Rust
at wasm
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668// SPDX-License-Identifier: Apache-2.0// SPDX-FileCopyrightText: 2021 The Gleam contributors
mod decision;mod expression;mod import;#[cfg(test)]mod tests;mod typescript;
use std::cell::RefCell;use std::collections::{HashMap, HashSet};use std::rc::Rc;
use debug_ignore::DebugIgnore;use num_bigint::BigInt;use num_traits::ToPrimitive;use sourcemap::SourceMap;
use crate::build::Target;use crate::build::package_compiler::StdlibPackage;use crate::codegen::TypeScriptDeclarations;use crate::line_numbers::LineColumn;use crate::type_::{PRELUDE_MODULE_NAME, RecordAccessor};use crate::{ ast::{Import, *}, line_numbers::LineNumbers,};use camino::Utf8Path;use ecow::{EcoString, eco_format};use expression::Context;use itertools::Itertools;use pretty_arena::*;
use self::import::{Imports, Member};
const INDENT: isize = 2;
pub const PRELUDE: &str = include_str!("../templates/prelude.mjs");pub const PRELUDE_TS_DEF: &str = include_str!("../templates/prelude.d.mts");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]pub enum JavaScriptCodegenTarget { JavaScript, TypeScriptDeclarations,}
/// A cursor position observer that does nothing./// Mainly used to allow us to create a cursor position observer that does/// nothing without having to check if the source map builder is present.#[derive(Debug, Clone, Copy)]pub struct NullCursorPositionObserver;
impl CursorPositionObserver for NullCursorPositionObserver { fn observe_cursor_position(&mut self, _line: isize, _width: isize) { // Do nothing }}
/// A cursor position observer that observes the cursor position and adds it/// to the source map builder. When notified it will take the destination/// location and map it to the initial source location given when/// creating the observer.#[derive(Debug)]pub struct SourceMapCursorPositionObserver { source_start_location: LineColumn, source_map_builder: Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>,}
impl SourceMapCursorPositionObserver { pub fn new( source_start_location: LineColumn, source_map_builder: Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>, ) -> Self { Self { source_start_location, source_map_builder, } }}
impl CursorPositionObserver for SourceMapCursorPositionObserver { fn observe_cursor_position(&mut self, line: isize, column: isize) { let _ = self.source_map_builder.borrow_mut().add_raw( line as u32, column as u32, // SourceMapBuilder expects 0-based line and column numbers and we use 1-based ones self.source_start_location.line - 1, self.source_start_location.column - 1, // Since we have a 1-1 mapping between source and destination locations // We always use 0 for the source index. Some(0), None, false, ); }}
#[derive(Debug)]pub struct Generator<'a> { line_numbers: &'a LineNumbers, module: &'a TypedModule, tracker: UsageTracker, module_scope: im::HashMap<EcoString, usize>, current_module_name_segments_count: usize, typescript: TypeScriptDeclarations, // Debug ignored since SourceMapBuilder doesn't implement debug source_map_builder: Option<Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>>, stdlib_package: StdlibPackage, /// Relative path to the module, surrounded in `"`s to make it a string, and with `\`s escaped /// to `\\`. src_path: EcoString,}
impl<'a, 'doc> Generator<'a> { pub fn new(config: ModuleConfig<'a>) -> Self { let ModuleConfig { typescript, source_map, stdlib_package, module, line_numbers, src: _, path: _, project_root, } = config; let current_module_name_segments_count = module.name.split('/').count();
let src_path = &module.type_info.src_path; let src_path = src_path .strip_prefix(project_root) .unwrap_or(src_path) .as_str(); let src_path = eco_format!("\"{src_path}\"").replace("\\", "\\\\"); Self { current_module_name_segments_count, line_numbers, module, src_path, tracker: UsageTracker::default(), module_scope: im::HashMap::new(), typescript, source_map_builder: if source_map { let module_name = module.name.clone(); let output_path = format!("{module_name}.mjs"); let module_alias = module_name.split('/').next_back().unwrap_or(&module_name); let input_path = format!("{module_alias}.gleam",); let mut source_map_builder = sourcemap::SourceMapBuilder::new(Some(&output_path)); let _ = source_map_builder.add_source(&input_path); Some(Rc::new(RefCell::new(DebugIgnore(source_map_builder)))) } else { None }, stdlib_package, } }
fn type_reference(&self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> { if self.typescript == TypeScriptDeclarations::None { return EMPTY_DOCUMENT; }
// Get the name of the module relative the directory (similar to basename) let module = self .module .name .as_str() .split('/') .next_back() .expect("JavaScript generator could not identify imported module name.");
docvec![ arena, REFERENCE_TYPES_DOCUMENT, module, DOT_D_DOT_MTS_CLOSE_QUOTE_CLOSE_TAG_DOCUMENT, LINE_DOCUMENT ] }
fn sourcemap_reference(&self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> { match self.source_map_builder { None => "".to_doc(arena), Some(_) => { // Get the name of the module relative the directory (similar to basename) let module = self .module .name .as_str() .split('/') .next_back() .expect("JavaScript generator could not identify imported module name.");
docvec![ arena, SOURCE_MAPPING_URL_EQUAL_DOCUMENT, module, DOT_MJS_DOT_MAP_DOCUMENT, LINE_DOCUMENT ] } } }
pub fn compile(&mut self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> { // Determine what JavaScript imports we need to generate let mut imports = self.collect_imports(arena);
// Determine what names are defined in the module scope so we know to // rename any variables that are defined within functions using the same // names. self.register_module_definitions_in_scope();
// Generate JavaScript code for each statement. let statements = self.definitions(arena);
// Two lines between each statement let no_statements = statements.is_empty(); let statements = arena.join(statements, TWO_LINES_DOCUMENT);
// Import any prelude functions that have been used
if self.tracker.ok_used { self.register_prelude_usage(arena, &mut imports, "Ok", None); }
if self.tracker.error_used { self.register_prelude_usage(arena, &mut imports, "Error", None); }
if self.tracker.list_used { self.register_prelude_usage(arena, &mut imports, "toList", None); }
if self.tracker.list_empty_class_used || self.tracker.echo_used { self.register_prelude_usage(arena, &mut imports, "Empty", Some("$Empty")); }
if self.tracker.list_empty_const_used { self.register_prelude_usage( arena, &mut imports, "List$Empty$const", Some("$List$Empty$const"), ); }
if self.tracker.list_non_empty_class_used || self.tracker.echo_used { self.register_prelude_usage(arena, &mut imports, "NonEmpty", Some("$NonEmpty")); }
if self.tracker.prepend_used { self.register_prelude_usage(arena, &mut imports, "prepend", Some("listPrepend")); }
if self.tracker.custom_type_used || self.tracker.echo_used { self.register_prelude_usage(arena, &mut imports, "CustomType", Some("$CustomType")); }
if self.tracker.make_error_used { self.register_prelude_usage(arena, &mut imports, "makeError", None); }
if self.tracker.int_remainder_used { self.register_prelude_usage(arena, &mut imports, "remainderInt", None); }
if self.tracker.float_division_used { self.register_prelude_usage(arena, &mut imports, "divideFloat", None); }
if self.tracker.int_division_used { self.register_prelude_usage(arena, &mut imports, "divideInt", None); }
if self.tracker.object_equality_used { self.register_prelude_usage(arena, &mut imports, "isEqual", None); }
if self.tracker.bit_array_literal_used { self.register_prelude_usage(arena, &mut imports, "toBitArray", None); }
if self.tracker.bit_array_slice_used || self.tracker.echo_used { self.register_prelude_usage(arena, &mut imports, "bitArraySlice", None); }
if self.tracker.bit_array_slice_to_float_used { self.register_prelude_usage(arena, &mut imports, "bitArraySliceToFloat", None); }
if self.tracker.bit_array_slice_to_int_used || self.tracker.echo_used { self.register_prelude_usage(arena, &mut imports, "bitArraySliceToInt", None); }
if self.tracker.sized_integer_segment_used { self.register_prelude_usage(arena, &mut imports, "sizedInt", None); }
if self.tracker.string_bit_array_segment_used { self.register_prelude_usage(arena, &mut imports, "stringBits", None); }
if self.tracker.string_utf16_bit_array_segment_used { self.register_prelude_usage(arena, &mut imports, "stringToUtf16", None); }
if self.tracker.string_utf32_bit_array_segment_used { self.register_prelude_usage(arena, &mut imports, "stringToUtf32", None); }
if self.tracker.codepoint_bit_array_segment_used { self.register_prelude_usage(arena, &mut imports, "codepointBits", None); }
if self.tracker.codepoint_utf16_bit_array_segment_used { self.register_prelude_usage(arena, &mut imports, "codepointToUtf16", None); }
if self.tracker.codepoint_utf32_bit_array_segment_used { self.register_prelude_usage(arena, &mut imports, "codepointToUtf32", None); }
if self.tracker.float_bit_array_segment_used { self.register_prelude_usage(arena, &mut imports, "sizedFloat", None); }
for (variant, alias) in self.tracker.variant_constants_used.iter() { let path = self.import_path(&variant.package, &variant.module); let member = Member { name: eco_format!("{}${}$const", variant.type_name, variant.name), alias: alias.as_ref().map(|alias| alias.to_doc(arena)), }; imports.register_module(path, [], [member]); }
// If we have some Gleam code that looks something like this: // ```gleam // import option.{None} // // pub fn main() { // None // } // ``` // // Here, the generated JavaScript code for `main` will look something // like this: // // ```javascript // export function main() { // return Option$None$const; // } // ``` // // The `None` variant is technically being imported, but we don't // actually use the generated `None` class in the JavaScript code, so // we don't want to import it. // // However, if we are pattern matching on the `None` variant, there // will be some code that looks like `if (value instanceof None)`, in // which case we still need to import `None`. Therefore we remove all // variants which are used as singleton constants, and that are not used // in pattern matching. // imports.filter_unused_variants( self.tracker .variant_constants_used .keys() .filter(|variant| !self.tracker.variants_used_in_instanceof.contains(variant)) .map(|variant| { let path = self.import_path(&variant.package, &variant.module); (path, variant.name.clone()) }), );
let echo_definition = self.echo_definition(arena, &mut imports); let sourcemap_reference = self.sourcemap_reference(arena); let type_reference = self.type_reference(arena); let filepath_definition = self.filepath_definition(arena);
// Put it all together
if imports.is_empty() && no_statements { docvec![ arena, sourcemap_reference, type_reference, filepath_definition, EXPORT_SPACE_OPEN_CLOSE_CURLY_DOCUMENT, LINE_DOCUMENT, echo_definition ] } else if imports.is_empty() { docvec![ arena, sourcemap_reference, type_reference, filepath_definition, statements.append(arena, LINE_DOCUMENT), echo_definition ] } else if no_statements { docvec![ arena, sourcemap_reference, type_reference, imports.into_doc(arena, JavaScriptCodegenTarget::JavaScript), filepath_definition, echo_definition, ] } else { docvec![ arena, sourcemap_reference, type_reference, imports.into_doc(arena, JavaScriptCodegenTarget::JavaScript), LINE_DOCUMENT, filepath_definition, statements, LINE_DOCUMENT, echo_definition ] } }
fn echo_definition( &mut self, arena: &'doc DocumentArena<'a, 'doc>, imports: &mut Imports<'a, 'doc>, ) -> Document<'a, 'doc> { if !self.tracker.echo_used { return EMPTY_DOCUMENT; }
if StdlibPackage::Present == self.stdlib_package { let value = Some(( AssignName::Variable("stdlib$dict".into()), SrcSpan::default(), ));
self.register_import(arena, imports, "gleam_stdlib", "gleam/dict", &value, &[]); } self.register_prelude_usage(arena, imports, "BitArray", Some("$BitArray")); self.register_prelude_usage(arena, imports, "List", Some("$List")); self.register_prelude_usage(arena, imports, "UtfCodepoint", Some("$UtfCodepoint")); docvec![ arena, LINE_DOCUMENT, std::include_str!("../templates/echo.mjs"), LINE_DOCUMENT ] }
fn register_prelude_usage( &self, arena: &'doc DocumentArena<'a, 'doc>, imports: &mut Imports<'a, 'doc>, name: &'static str, alias: Option<&'static str>, ) { let path = self.import_path(&self.module.type_info.package, PRELUDE_MODULE_NAME); let member = Member { name: name.into(), alias: alias.map(|a| a.to_doc(arena)), }; imports.register_module(path, [], [member]); }
fn custom_type_definition( &mut self, arena: &'doc DocumentArena<'a, 'doc>, custom_type: &'a TypedCustomType, ) -> Option<Vec<Document<'a, 'doc>>> { if self .module .unused_definition_positions .contains(&custom_type.location.start) { return None; }
let TypedCustomType { name, publicity, constructors, opaque, .. } = custom_type;
// If there's no constructors then there's nothing to do here. if constructors.is_empty() { return Some(vec![]); }
self.tracker.custom_type_used = true;
let constructor_publicity = if *opaque || publicity.is_private() { Publicity::Private } else { Publicity::Public };
let mut definitions = constructors .iter() .map(|constructor| { self.variant_definition(arena, constructor, name, constructor_publicity) }) .collect_vec();
// Generate getters for fields shared between variants if let Some(accessors_map) = self.module.type_info.accessors.get(name) && !accessors_map.shared_accessors.is_empty() // Don't bother generating shared getters when there's only one variant, // since the specific accessors can always be uses instead. && constructors.len() != 1 // Only generate accessors for the API if the constructors are public && constructor_publicity.is_public() { definitions.push(self.shared_custom_type_fields( arena, name, &accessors_map.shared_accessors, )); } // Add start and end position source map trackers to the first and last // definition in place. This is to prevent extra new lines from being added // to the output Since each definition is a separate statement in the output. let start_location = custom_type .documentation .as_ref() // 3 is the length of the "///" documentation marker. // Start is the index of the actual content, so we need to subtract // the length of the marker. .map_or(custom_type.location.start, |(start, _)| *start - 3); let first_definition = definitions.remove(0); definitions.insert( 0, self.source_map_tracker(arena, start_location) .append(arena, first_definition), ); let last_definition = definitions .pop() .expect("Custom type must have at least one definition here"); definitions.push(last_definition.append( arena, self.source_map_tracker(arena, custom_type.end_position), ));
Some(definitions) }
fn variant_definition( &self, arena: &'doc DocumentArena<'a, 'doc>, constructor: &'a TypedRecordConstructor, type_name: &'a str, publicity: Publicity, ) -> Document<'a, 'doc> { let class_definition = self.variant_class_definition(arena, constructor, publicity);
// Singleton constants are used internally by the compiler, so they aren't // part of the public JS API and need to be generated even for private // types. let constructor_singleton = if constructor.arguments.is_empty() { docvec![ arena, LINE_DOCUMENT, self.variant_constructor_constant(arena, constructor, type_name, publicity) ] } else { EMPTY_DOCUMENT };
// If the custom type is private or opaque, we don't need to generate API // functions for it. if publicity.is_private() { return class_definition.append(arena, constructor_singleton); }
let constructor_definition = self.variant_constructor_definition(arena, constructor, type_name); let variant_check_definition = self.variant_check_definition(arena, constructor, type_name); let fields_definition = self.variant_fields_definition(arena, constructor, type_name);
docvec![ arena, class_definition, constructor_singleton, LINE_DOCUMENT, constructor_definition, LINE_DOCUMENT, variant_check_definition, fields_definition, ] }
/// Generate a singleton constant for a variant with no fields. This means /// that all values of a particular variant are the same underlying reference, /// allowing them to be compared more efficiently. /// fn variant_constructor_constant( &self, arena: &'doc DocumentArena<'a, 'doc>, constructor: &'a TypedRecordConstructor, type_name: &'a str, publicity: Publicity, ) -> Document<'a, 'doc> { let keyword = if publicity.is_importable() { EXPORT_CONST_SPACE_DOCUMENT } else { CONST_SPACE_DOCUMENT }; docvec![ arena, self.source_map_tracker(arena, constructor.location.start), keyword, type_name, DOLLAR_DOCUMENT, constructor.name.as_str(), DOLLAR_CONST_DOCUMENT, SPACE_EQUAL_DOCUMENT, docvec![ arena, BREAKABLE_SPACE_DOCUMENT, NEW_SPACE_DOCUMENT, constructor.name.as_str(), OPEN_CLOSE_PAREN_SEMICOLON_DOCUMENT, ] .group(arena) .nest(arena, INDENT) ] }
fn variant_constructor_definition( &self, arena: &'doc DocumentArena<'a, 'doc>, constructor: &'a TypedRecordConstructor, type_name: &'a str, ) -> Document<'a, 'doc> { // If the constructor has no fields, return the singleton constant // instead. if constructor.arguments.is_empty() { return docvec![ arena, EXPORT_CONST_SPACE_DOCUMENT, type_name, DOLLAR_DOCUMENT, constructor.name.as_str(), SPACE_EQUAL_OPEN_CLOSE_PAREN_SPACE_ARROW_DOCUMENT, docvec![ arena, BREAKABLE_SPACE_DOCUMENT, type_name, DOLLAR_DOCUMENT, constructor.name.as_str(), DOLLAR_CONST_SEMICOLON_DOCUMENT ] .group(arena) .nest(arena, INDENT), ]; }
let mut arguments = Vec::new();
for (index, parameter) in constructor.arguments.iter().enumerate() { if let Some((_, label)) = ¶meter.label { arguments.push(maybe_escape_identifier(label).to_doc(arena)); } else { arguments.push(eco_format!("${index}").to_doc(arena)); } }
let construction = docvec![ arena, BREAKABLE_SPACE_DOCUMENT, NEW_SPACE_DOCUMENT, constructor.name.as_str(), OPEN_PAREN_DOCUMENT, arena .join(arguments.clone(), COMMA_BREAK_DOCUMENT) .group(arena), CLOSE_PAREN_SEMICOLON_DOCUMENT, ] .group(arena);
docvec![ arena, self.source_map_tracker(arena, constructor.location.start), EXPORT_CONST_SPACE_DOCUMENT, type_name, DOLLAR_DOCUMENT, constructor.name.as_str(), SPACE_EQUAL_SPACE_OPEN_PAREN_DOCUMENT, arena.join(arguments, COMMA_BREAK_DOCUMENT), CLOSE_PAREN_ARROW_DOCUMENT, construction.nest(arena, INDENT), ] }
fn variant_check_definition( &self, arena: &'doc DocumentArena<'a, 'doc>, constructor: &'a TypedRecordConstructor, type_name: &'a str, ) -> Document<'a, 'doc> { let construction = docvec![ arena, BREAKABLE_SPACE_DOCUMENT, VALUE_INSTANCE_OF_SPACE_DOCUMENT, constructor.name.as_str(), SEMICOLON_DOCUMENT ] .group(arena);
docvec![ arena, self.source_map_tracker(arena, constructor.location.start), EXPORT_CONST_SPACE_DOCUMENT, type_name, DOLLAR_IS_DOCUMENT, constructor.name.as_str(), SPACE_EQUAL_SPACE_VALUE_SPACE_ARROW, construction.nest(arena, INDENT), ] }
fn variant_fields_definition( &self, arena: &'doc DocumentArena<'a, 'doc>, constructor: &'a TypedRecordConstructor, type_name: &'a str, ) -> Document<'a, 'doc> { let mut functions = Vec::new();
for (index, argument) in constructor.arguments.iter().enumerate() { // Always generate the accessor for the value at this index. Although // this is not necessary when a label is present, we want to make sure // that adding a label to a record isn't a breaking change. For this // reason, we need to generate an index getter even when a label is // present to ensure consistent behaviour between labelled and unlabelled // field access. let function_name = eco_format!( "{type_name}${record_name}${index}", record_name = constructor.name, );
let contents;
// If the argument is labelled, also generate a getter for the labelled // argument. if let Some((_, label)) = &argument.label { let function_name = eco_format!( "{type_name}${record_name}${label}", record_name = constructor.name, );
contents = docvec![ arena, BREAKABLE_SPACE_DOCUMENT, VALUE_DOT_DOCUMENT, maybe_escape_property(label), SEMICOLON_DOCUMENT ] .group(arena);
functions.push(docvec![ arena, LINE_DOCUMENT, self.source_map_tracker(arena, constructor.location.start), EXPORT_CONST_SPACE_DOCUMENT, function_name, SPACE_EQUAL_SPACE_VALUE_SPACE_ARROW, contents.nest(arena, INDENT), ]); } else { contents = docvec![ arena, BREAKABLE_SPACE_DOCUMENT, VALUE_OPEN_SQUARE_DOCUMENT, index, CLOSE_SQUARE_SEMICOLON_DOCUMENT ] .group(arena); }
functions.push(docvec![ arena, LINE_DOCUMENT, self.source_map_tracker(arena, constructor.location.start), EXPORT_CONST_SPACE_DOCUMENT, function_name, SPACE_EQUAL_SPACE_VALUE_SPACE_ARROW, contents.nest(arena, INDENT), ]); }
arena.concat(functions) }
fn shared_custom_type_fields( &self, arena: &'doc DocumentArena<'a, 'doc>, type_name: &'a str, shared_accessors: &HashMap<EcoString, RecordAccessor>, ) -> Document<'a, 'doc> { let accessors = shared_accessors.keys().sorted().map(|field| { let function_name = eco_format!("{type_name}${field}");
let contents = docvec![ arena, BREAKABLE_SPACE_DOCUMENT, VALUE_DOT_DOCUMENT, maybe_escape_property(field), SEMICOLON_DOCUMENT ] .group(arena);
docvec![ arena, EXPORT_CONST_SPACE_DOCUMENT, function_name, SPACE_EQUAL_SPACE_VALUE_SPACE_ARROW, contents.nest(arena, INDENT), ] }); arena.join(accessors, LINE_DOCUMENT) }
fn variant_class_definition( &self, arena: &'doc DocumentArena<'a, 'doc>, constructor: &'a TypedRecordConstructor, publicity: Publicity, ) -> Document<'a, 'doc> { fn parameter<'a, 'doc>( arena: &'doc DocumentArena<'a, 'doc>, (i, arg): (usize, &TypedRecordConstructorArg), ) -> Document<'a, 'doc> { arg.label .as_ref() .map(|(_, s)| maybe_escape_identifier(s)) .unwrap_or_else(|| eco_format!("${i}")) .to_doc(arena) }
let doc = if let Some((start, documentation)) = &constructor.documentation { docvec![ arena, // 3 is the length of the "///" documentation marker. // Start is the index of the actual content, so we need to subtract // the length of the marker. self.source_map_tracker(arena, *start - 3), jsdoc_comment(arena, documentation, publicity), LINE_DOCUMENT ] } else { EMPTY_DOCUMENT };
let head = if publicity.is_public() { EXPORT_CLASS_SPACE_DOCUMENT } else { CLASS_SPACE_DOCUMENT };
let head = docvec![ arena, self.source_map_tracker(arena, constructor.location.start), head, &constructor.name, SPACE_EXTENDS_CUSTOM_TYPE_OPEN_CURLY_DOCUMENT ];
if constructor.arguments.is_empty() { return docvec![arena, doc, head, CLOSE_CURLY_DOCUMENT]; }
let parameters = arena.join( constructor .arguments .iter() .enumerate() .map(|argument| parameter(arena, argument)), COMMA_BREAK_DOCUMENT, );
let constructor_body = arena.join( constructor.arguments.iter().enumerate().map(|(i, arg)| { let var = parameter(arena, (i, arg)); match &arg.label { None => docvec![ arena, THIS_OPEN_SQUARE_DOCUMENT, i, CLOSE_SQUARE_SPACE_EQUAL_SPACE_DOCUMENT, var, SEMICOLON_DOCUMENT ], Some((_, name)) => { docvec![ arena, THIS_DOT_DOCUMENT, maybe_escape_property(name), SPACE_EQUAL_SPACE_DOCUMENT, var, SEMICOLON_DOCUMENT ] } } }), LINE_DOCUMENT, );
let class_body = docvec![ arena, LINE_DOCUMENT, CONSTRUCTOR_OPEN_PAREN_DOCUMENT, parameters, CLOSE_PAREN_SPACE_OPEN_CURLY_DOCUMENT, docvec![ arena, LINE_DOCUMENT, SUPER_CALL_SEMICOLON_DOCUMENT, LINE_DOCUMENT, constructor_body ] .nest(arena, INDENT), LINE_DOCUMENT, CLOSE_CURLY_DOCUMENT, ] .nest(arena, INDENT);
docvec![arena, doc, head, class_body, LINE_DOCUMENT, "}"] }
fn definitions(&mut self, arena: &'doc DocumentArena<'a, 'doc>) -> Vec<Document<'a, 'doc>> { let mut definitions = vec![];
for custom_type in &self.module.definitions.custom_types { if let Some(mut new_definitions) = self.custom_type_definition(arena, custom_type) { definitions.append(&mut new_definitions); } }
for constant in &self.module.definitions.constants { if let Some(definition) = self.module_constant(arena, constant) { definitions.push(definition); } }
for function in &self.module.definitions.functions { if let Some(definition) = self.module_function(arena, function) { definitions.push(definition); } }
definitions }
fn collect_imports(&mut self, arena: &'doc DocumentArena<'a, 'doc>) -> Imports<'a, 'doc> { let mut imports = Imports::new();
for Import { module, as_name, unqualified_values, package, .. } in &self.module.definitions.imports { self.register_import( arena, &mut imports, package, module, as_name, unqualified_values, ); }
for function in &self.module.definitions.functions { if let Some((_, name)) = &function.name && let Some((module, external_function, _)) = &function.external_javascript { self.register_external_function( arena, &mut imports, function.publicity, name, module, external_function, ); } }
imports }
fn import_path(&self, package: &str, module: &str) -> EcoString { // TODO: strip shared prefixed between current module and imported // module to avoid descending and climbing back out again if package == self.module.type_info.package || package.is_empty() { // Same package match self.current_module_name_segments_count { 1 => eco_format!("./{module}.mjs"), _ => { let prefix = "../".repeat(self.current_module_name_segments_count - 1); eco_format!("{prefix}{module}.mjs") } } } else { // Different package let prefix = "../".repeat(self.current_module_name_segments_count); eco_format!("{prefix}{package}/{module}.mjs") } }
fn register_import( &mut self, arena: &'doc DocumentArena<'a, 'doc>, imports: &mut Imports<'a, 'doc>, package: &'a str, module: &'a str, as_name: &Option<(AssignName, SrcSpan)>, unqualified: &[UnqualifiedImport], ) { let get_name = |module: &'a str| { module .split('/') .next_back() .expect("JavaScript generator could not identify imported module name.") };
let (discarded, module_name) = match as_name { None => (false, get_name(module)), Some((AssignName::Discard(_), _)) => (true, get_name(module)), Some((AssignName::Variable(name), _)) => (false, name.as_str()), };
let module_name = eco_format!("${module_name}"); let path = self.import_path(package, module); let unqualified_imports = unqualified.iter().map(|i| { let alias = i.as_name.as_ref().map(|n| { self.register_in_scope(n); maybe_escape_identifier(n).to_doc(arena) }); let name = maybe_escape_identifier(&i.name); Member { name, alias } });
let aliases = if discarded { vec![] } else { vec![module_name] }; imports.register_module(path, aliases, unqualified_imports); }
fn register_external_function( &mut self, arena: &'doc DocumentArena<'a, 'doc>, imports: &mut Imports<'a, 'doc>, publicity: Publicity, name: &'a str, module: &'a str, fun: &EcoString, ) { let needs_escaping = !is_usable_js_identifier(name); let member = Member { name: fun.clone(), alias: if name == fun && !needs_escaping { None } else if needs_escaping { Some(escape_identifier(name).to_doc(arena)) } else { Some(name.to_doc(arena)) }, }; if publicity.is_importable() { imports.register_export(maybe_escape_identifier_string(name)); } imports.register_module(EcoString::from(module), [], [member]); }
fn module_constant( &mut self, arena: &'doc DocumentArena<'a, 'doc>, constant: &'a TypedModuleConstant, ) -> Option<Document<'a, 'doc>> { let TypedModuleConstant { documentation, location, publicity, name, value, .. } = constant;
// We don't generate any code for unused constants. if self .module .unused_definition_positions .contains(&location.start) { return None; }
let head = if publicity.is_private() { CONST_SPACE_DOCUMENT } else { EXPORT_CONST_SPACE_DOCUMENT };
let mut generator = expression::Generator::new( self.module.name.clone(), self.src_path.clone(), self.line_numbers, "".into(), vec![], &mut self.tracker, self.module_scope.clone(), self.source_map_builder.clone(), );
let document = generator.constant_expression(arena, Context::Constant, value);
let jsdoc = if let Some((start, documentation)) = documentation { docvec![ arena, // 3 is the length of the "///" documentation marker. // Start is the index of the actual content, so we need to subtract // the length of the marker. self.source_map_tracker(arena, *start - 3), jsdoc_comment(arena, documentation, *publicity), LINE_DOCUMENT ] } else { EMPTY_DOCUMENT };
Some(docvec![ arena, jsdoc, self.source_map_tracker(arena, location.start), head, maybe_escape_identifier(name), SPACE_EQUAL_SPACE_DOCUMENT, document, SEMICOLON_DOCUMENT, self.source_map_tracker(arena, value.location().end), ]) }
fn register_in_scope(&mut self, name: &str) { let _ = self.module_scope.insert(name.into(), 0); }
fn module_function( &mut self, arena: &'doc DocumentArena<'a, 'doc>, function: &'a TypedFunction, ) -> Option<Document<'a, 'doc>> { // We don't generate any code for unused functions. if self .module .unused_definition_positions .contains(&function.location.start) { return None; }
// If there's an external JavaScript implementation then it will be imported, // so we don't need to generate a function definition. if function.external_javascript.is_some() { return None; }
// If the function does not support JavaScript then we don't need to generate // a function definition. if !function.implementations.supports(Target::JavaScript) { return None; } let function_source_mapping = self.source_map_tracker(arena, function.location.start);
let (_, name) = function .name .as_ref() .expect("A module's function must be named"); let argument_names = function .arguments .iter() .map(|arg| arg.names.get_variable_name()) .collect();
let function_doc = match &function.documentation { None => EMPTY_DOCUMENT, Some((start, documentation)) => { docvec![ arena, // 3 is the length of the "///" documentation marker. // Start is the index of the actual content, so we need to subtract // the length of the marker. self.source_map_tracker(arena, *start - 3), jsdoc_comment(arena, documentation, function.publicity), LINE_DOCUMENT ] } };
let mut generator = expression::Generator::new( self.module.name.clone(), self.src_path.clone(), self.line_numbers, name.clone(), argument_names, &mut self.tracker, self.module_scope.clone(), self.source_map_builder.clone(), );
let head = if function.publicity.is_private() { FUNCTION_SPACE_DOCUMENT } else { EXPORT_FUNCTION_SPACE_DOCUMENT };
let body = generator.function_body( arena, function.body.as_slice(), function.arguments.as_slice(), );
Some(docvec![ arena, function_doc, function_source_mapping, head, maybe_escape_identifier(name.as_str()), fun_arguments( arena, function.arguments.as_slice(), generator.tail_recursion_used ), SPACE_OPEN_CURLY_DOCUMENT, docvec![arena, LINE_DOCUMENT, body] .nest(arena, INDENT) .group(arena), LINE_DOCUMENT, CLOSE_CURLY_DOCUMENT, self.source_map_tracker(arena, function.end_position), ]) }
fn register_module_definitions_in_scope(&mut self) { for constant in &self.module.definitions.constants { self.register_in_scope(&constant.name); }
for function in &self.module.definitions.functions { if let Some((_, name)) = &function.name { self.register_in_scope(name); } }
for import in &self.module.definitions.imports { for unqualified_value in &import.unqualified_values { self.register_in_scope(unqualified_value.used_name()); } } }
fn filepath_definition(&self, arena: &'doc DocumentArena<'a, 'doc>) -> Document<'a, 'doc> { if !self.tracker.make_error_used { return EMPTY_DOCUMENT; }
docvec![ arena, CONST_FILEPATH_EQUALS_DOCUMENT, self.src_path.clone(), SEMICOLON_DOCUMENT, TWO_LINES_DOCUMENT ] }
fn source_map_tracker( &self, arena: &'doc DocumentArena<'a, 'doc>, start_index: u32, ) -> Document<'a, 'doc> { create_cursor_position_observer( arena, &self.source_map_builder, self.line_numbers, start_index, ) }}
fn jsdoc_comment<'a, 'doc>( arena: &'doc DocumentArena<'a, 'doc>, documentation: &EcoString, publicity: Publicity,) -> Document<'a, 'doc> { // We start with the documentation of the function let doc_body = arena.join( documentation .trim_end() .split('\n') // Each comment line is turned into a zero width string. // We actually don't need to know how long a comment string is // because comments will never be split anyway. // So this saves us a lot of useless grapheme counting that would // otherwise have to happen if we just turned the string into a // regular document. .map(|line| arena.zero_width_string(eco_format!(" *{}", line.replace("*/", "*\\/")))), LINE_DOCUMENT, );
let mut doc = docvec![ arena, SLASH_TIMES_TIMES_DOCUMENT, LINE_DOCUMENT, doc_body, LINE_DOCUMENT ]; if !publicity.is_public() { // If the function is not public we hide the documentation using // the `@ignore` tag: https://jsdoc.app/tags-ignore doc = docvec![ arena, doc, SPACE_TIMES_SPACE_DOCUMENT, LINE_DOCUMENT, SPACE_TIMES_AT_IGNORE_DOCUMENT, LINE_DOCUMENT ]; } // And finally we close the doc comment docvec![arena, doc, SPACE_TIMES_SLASH_DOCUMENT]}
#[derive(Debug)]pub struct ModuleConfig<'a> { pub module: &'a TypedModule, pub line_numbers: &'a LineNumbers, pub src: &'a EcoString, pub typescript: TypeScriptDeclarations, pub source_map: bool, pub stdlib_package: StdlibPackage, pub path: &'a Utf8Path, pub project_root: &'a Utf8Path,}
pub fn module(config: ModuleConfig<'_>) -> (String, Option<SourceMap>) { let (output, sourcemap_builder) = { let arena = DocumentArena::new(); let mut generator = Generator::new(config); let document = generator.compile(&arena).to_pretty_string(80); let builder = generator.source_map_builder; (document, builder) };
let source_map = sourcemap_builder.map(|builder| { // We have completed the generation of the module, so we can now take ownership // of the builder. Rc::try_unwrap(builder) .expect("Failed to take ownership of sourcemap builder") .into_inner() .0 .into_sourcemap() }); (output, source_map)}
pub fn ts_declaration(module: &TypedModule) -> String { let arena = DocumentArena::new(); let document = typescript::TypeScriptGenerator::new(module).compile(&arena); document.to_pretty_string(80)}
fn create_cursor_position_observer<'a, 'doc>( arena: &'doc DocumentArena<'a, 'doc>, builder: &Option<Rc<RefCell<DebugIgnore<sourcemap::SourceMapBuilder>>>>, line_numbers: &LineNumbers, start_index: u32,) -> Document<'a, 'doc> { let start_location = line_numbers.line_and_column_number(start_index); arena.position_observer(match builder { None => Rc::new(RefCell::new(NullCursorPositionObserver)), Some(builder) => Rc::new(RefCell::new(SourceMapCursorPositionObserver::new( start_location, builder.clone(), ))), })}
fn fun_arguments<'a, 'doc>( arena: &'doc DocumentArena<'a, 'doc>, arguments: &'a [TypedArg], tail_recursion_used: bool,) -> Document<'a, 'doc> { let mut discards = 0; wrap_arguments( arena, arguments .iter() .map(|argument| match argument.get_variable_name() { None => { let doc = if discards == 0 { UNDERSCORE_DOCUMENT } else { eco_format!("_{discards}").to_doc(arena) }; discards += 1; doc } Some(name) if tail_recursion_used => eco_format!("loop${name}").to_doc(arena), Some(name) => maybe_escape_identifier(name).to_doc(arena), }), )}
fn wrap_arguments<'a, 'doc>( arena: &'doc DocumentArena<'a, 'doc>, arguments: impl IntoIterator<Item = Document<'a, 'doc>>,) -> Document<'a, 'doc> { EMPTY_BREAK_DOCUMENT .append(arena, arena.join(arguments, COMMA_BREAK_DOCUMENT)) .nest(arena, INDENT) .append(arena, EMPTY_BREAK_DOCUMENT) .surround(arena, OPEN_PAREN_DOCUMENT, CLOSE_PAREN_DOCUMENT) .group(arena)}
fn wrap_object<'a, 'doc>( arena: &'doc DocumentArena<'a, 'doc>, items: impl IntoIterator<Item = (Document<'a, 'doc>, Option<Document<'a, 'doc>>)>,) -> Document<'a, 'doc> { let mut empty = true; let fields = items.into_iter().map(|(key, value)| { empty = false; match value { Some(value) => docvec![arena, key, COLON_SPACE_DOCUMENT, value], None => key.to_doc(arena), } }); let fields = arena.join(fields, COMMA_BREAK_DOCUMENT);
if empty { OPEN_CLOSE_CURLY_DOCUMENT } else { docvec![ arena, docvec![arena, OPEN_CURLY_DOCUMENT, BREAKABLE_SPACE_DOCUMENT, fields] .nest(arena, INDENT) .append(arena, BREAKABLE_SPACE_DOCUMENT) .group(arena), CLOSE_CURLY_DOCUMENT ] }}
fn is_usable_js_identifier(word: &str) -> bool { !matches!( word, // Keywords and reserved words // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar "await" | "arguments" | "break" | "case" | "catch" | "class" | "const" | "continue" | "debugger" | "default" | "delete" | "do" | "else" | "enum" | "export" | "extends" | "eval" | "false" | "finally" | "for" | "function" | "if" | "implements" | "import" | "in" | "instanceof" | "interface" | "let" | "new" | "null" | "package" | "private" | "protected" | "public" | "return" | "static" | "super" | "switch" | "this" | "throw" | "true" | "try" | "typeof" | "var" | "void" | "while" | "with" | "yield" // `undefined` to avoid any unintentional overriding. | "undefined" // `then` to avoid a module that defines a `then` function being // used as a `thenable` in JavaScript when the module is imported // dynamically, which results in unexpected behaviour. // It is rather unfortunate that we have to do this. | "then" )}
fn is_usable_js_property(label: &str) -> bool { match label { // `then` to avoid a custom type that defines a `then` function being // used as a `thenable` in Javascript. "then" // `constructor` to avoid unintentional overriding of the constructor of // records, leading to potential runtime crashes while using `withFields`. | "constructor" // `prototype` and `__proto__` to avoid unintentionally overriding the // prototype chain. | "prototype" | "__proto__" => false, _ => true }}
fn maybe_escape_identifier_string(word: &str) -> EcoString { if is_usable_js_identifier(word) { EcoString::from(word) } else { escape_identifier(word) }}
fn escape_identifier(word: &str) -> EcoString { eco_format!("{word}$")}
fn maybe_escape_identifier(word: &str) -> EcoString { if is_usable_js_identifier(word) { EcoString::from(word) } else { escape_identifier(word) }}
fn maybe_escape_property(label: &str) -> EcoString { if is_usable_js_property(label) { EcoString::from(label) } else { escape_identifier(label) }}
#[derive(Debug, Default)]pub(crate) struct UsageTracker { pub ok_used: bool, pub list_used: bool, pub list_empty_class_used: bool, pub list_empty_const_used: bool, pub list_non_empty_class_used: bool, pub prepend_used: bool, pub error_used: bool, pub int_remainder_used: bool, pub make_error_used: bool, pub custom_type_used: bool, pub int_division_used: bool, pub float_division_used: bool, pub object_equality_used: bool, pub bit_array_literal_used: bool, pub bit_array_slice_used: bool, pub bit_array_slice_to_float_used: bool, pub bit_array_slice_to_int_used: bool, pub sized_integer_segment_used: bool, pub string_bit_array_segment_used: bool, pub string_utf16_bit_array_segment_used: bool, pub string_utf32_bit_array_segment_used: bool, pub codepoint_bit_array_segment_used: bool, pub codepoint_utf16_bit_array_segment_used: bool, pub codepoint_utf32_bit_array_segment_used: bool, pub float_bit_array_segment_used: bool, pub echo_used: bool, /// Keeps track of each time a fieldless variant is constructed, so we can /// import the singleton constant. Optionally contains an alias, if the variant /// was imported unqualified and aliased. pub variant_constants_used: HashMap<TypeVariant, Option<EcoString>>, /// Fieldless variants that are used in `instanceof` checks during pattern /// matching or comparison. If we're only using a fieldless variant for /// construction, we don't need to import the variant class itself, just it /// singleton constant. However, if we are using `instanceof`, we still need /// to import it. pub variants_used_in_instanceof: HashSet<TypeVariant>,}
#[derive(Debug, PartialEq, Eq, Hash)]pub struct TypeVariant { package: EcoString, module: EcoString, type_name: EcoString, name: EcoString,}
fn bool(bool: bool) -> Document<'static, 'static> { match bool { true => TRUE_LOWERCASE_DOCUMENT, false => FALSE_LOWERCASE_DOCUMENT, }}
/// Int segments <= 48 bits wide in bit arrays are within JavaScript's safe range and are evaluated/// at compile time when all inputs are known. This is done for both bit array expressions and/// pattern matching.////// Int segments of any size could be evaluated at compile time, but currently aren't due to the/// potential for causing large generated JS for inputs such as `<<0:8192>>`.///pub(crate) const SAFE_INT_SEGMENT_MAX_SIZE: usize = 48;
/// Evaluates the value of an Int segment in a bit array into its corresponding bytes. This avoids/// needing to do the evaluation at runtime when all inputs are known at compile-time.///pub(crate) fn bit_array_segment_int_value_to_bytes( mut value: BigInt, size: BigInt, endianness: Endianness,) -> Vec<u8> { // Clamp negative sizes to zero let size = size.max(BigInt::ZERO);
// Convert size to u32. This is safe because this function isn't called with a size greater // than `SAFE_INT_SEGMENT_MAX_SIZE`. let size = size .to_u32() .expect("bit array segment size to be a valid u32");
// Convert negative number to two's complement representation if value < BigInt::ZERO { let value_modulus = BigInt::from(2).pow(size); value = &value_modulus + (value % &value_modulus); }
// Convert value to the desired number of bytes let mut bytes = vec![0u8; size as usize / 8]; for byte in bytes.iter_mut() { *byte = (&value % BigInt::from(256)) .to_u8() .expect("modulo result to be a valid u32"); value /= BigInt::from(256); }
if endianness.is_big() { bytes.reverse(); }
bytes}