mod decision; mod expression; mod import; #[cfg(test)] mod tests; mod typescript; use num_bigint::BigInt; use num_traits::ToPrimitive; use crate::analyse::TargetSupport; use crate::build::Target; use crate::build::package_compiler::StdlibPackage; use crate::codegen::TypeScriptDeclarations; use crate::type_::PRELUDE_MODULE_NAME; use crate::{ ast::{CustomType, Function, Import, ModuleConstant, TypeAlias, *}, docvec, line_numbers::LineNumbers, pretty::*, }; use camino::Utf8Path; use ecow::{EcoString, eco_format}; use expression::Context; use itertools::Itertools; 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"); pub type Output<'a> = Result, Error>; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum JavaScriptCodegenTarget { JavaScript, TypeScriptDeclarations, } #[derive(Debug)] pub struct Generator<'a> { line_numbers: &'a LineNumbers, module: &'a TypedModule, tracker: UsageTracker, module_scope: im::HashMap, current_module_name_segments_count: usize, target_support: TargetSupport, typescript: TypeScriptDeclarations, 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> Generator<'a> { pub fn new(config: ModuleConfig<'a>) -> Self { let ModuleConfig { target_support, typescript, 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: Default::default(), target_support, typescript, stdlib_package, } } fn type_reference(&self) -> Document<'a> { if self.typescript == TypeScriptDeclarations::None { return nil(); } // 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!["/// ", line()] } pub fn compile(&mut self) -> Output<'a> { // Determine what JavaScript imports we need to generate let mut imports = self.collect_imports(); // 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.collect_definitions().into_iter().chain( self.module .definitions .iter() .flat_map(|definition| self.definition(definition)), ); // Two lines between each statement let mut statements: Vec<_> = Itertools::intersperse(statements, Ok(lines(2))).try_collect()?; // Import any prelude functions that have been used if self.tracker.ok_used { self.register_prelude_usage(&mut imports, "Ok", None); }; if self.tracker.error_used { self.register_prelude_usage(&mut imports, "Error", None); }; if self.tracker.list_used { self.register_prelude_usage(&mut imports, "toList", None); }; if self.tracker.list_empty_class_used || self.tracker.echo_used { self.register_prelude_usage(&mut imports, "Empty", Some("$Empty")); }; if self.tracker.list_non_empty_class_used || self.tracker.echo_used { self.register_prelude_usage(&mut imports, "NonEmpty", Some("$NonEmpty")); }; if self.tracker.prepend_used { self.register_prelude_usage(&mut imports, "prepend", Some("listPrepend")); }; if self.tracker.custom_type_used || self.tracker.echo_used { self.register_prelude_usage(&mut imports, "CustomType", Some("$CustomType")); }; if self.tracker.make_error_used { self.register_prelude_usage(&mut imports, "makeError", None); }; if self.tracker.int_remainder_used { self.register_prelude_usage(&mut imports, "remainderInt", None); }; if self.tracker.float_division_used { self.register_prelude_usage(&mut imports, "divideFloat", None); }; if self.tracker.int_division_used { self.register_prelude_usage(&mut imports, "divideInt", None); }; if self.tracker.object_equality_used { self.register_prelude_usage(&mut imports, "isEqual", None); }; if self.tracker.bit_array_literal_used { self.register_prelude_usage(&mut imports, "toBitArray", None); } if self.tracker.bit_array_slice_used || self.tracker.echo_used { self.register_prelude_usage(&mut imports, "bitArraySlice", None); } if self.tracker.bit_array_slice_to_float_used { self.register_prelude_usage(&mut imports, "bitArraySliceToFloat", None); } if self.tracker.bit_array_slice_to_int_used || self.tracker.echo_used { self.register_prelude_usage(&mut imports, "bitArraySliceToInt", None); } if self.tracker.sized_integer_segment_used { self.register_prelude_usage(&mut imports, "sizedInt", None); } if self.tracker.string_bit_array_segment_used { self.register_prelude_usage(&mut imports, "stringBits", None); } if self.tracker.string_utf16_bit_array_segment_used { self.register_prelude_usage(&mut imports, "stringToUtf16", None); } if self.tracker.string_utf32_bit_array_segment_used { self.register_prelude_usage(&mut imports, "stringToUtf32", None); } if self.tracker.codepoint_bit_array_segment_used { self.register_prelude_usage(&mut imports, "codepointBits", None); } if self.tracker.codepoint_utf16_bit_array_segment_used { self.register_prelude_usage(&mut imports, "codepointToUtf16", None); } if self.tracker.codepoint_utf32_bit_array_segment_used { self.register_prelude_usage(&mut imports, "codepointToUtf32", None); } if self.tracker.float_bit_array_segment_used { self.register_prelude_usage(&mut imports, "sizedFloat", None); } let echo_definition = self.echo_definition(&mut imports); let type_reference = self.type_reference(); let filepath_definition = self.filepath_definition(); // Put it all together if imports.is_empty() && statements.is_empty() { Ok(docvec![ type_reference, filepath_definition, "export {}", line(), echo_definition ]) } else if imports.is_empty() { statements.push(line()); Ok(docvec![ type_reference, filepath_definition, statements, echo_definition ]) } else if statements.is_empty() { Ok(docvec![ type_reference, imports.into_doc(JavaScriptCodegenTarget::JavaScript), filepath_definition, echo_definition, ]) } else { Ok(docvec![ type_reference, imports.into_doc(JavaScriptCodegenTarget::JavaScript), line(), filepath_definition, statements, line(), echo_definition ]) } } fn echo_definition(&mut self, imports: &mut Imports<'a>) -> Document<'a> { if !self.tracker.echo_used { return nil(); } if StdlibPackage::Present == self.stdlib_package { let value = Some(( AssignName::Variable("stdlib$dict".into()), SrcSpan::default(), )); self.register_import(imports, "gleam_stdlib", "dict", &value, &[]); } self.register_prelude_usage(imports, "BitArray", Some("$BitArray")); self.register_prelude_usage(imports, "List", Some("$List")); self.register_prelude_usage(imports, "UtfCodepoint", Some("$UtfCodepoint")); docvec![line(), std::include_str!("../templates/echo.mjs"), line()] } fn register_prelude_usage( &self, imports: &mut Imports<'a>, 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.to_doc(), alias: alias.map(|a| a.to_doc()), }; imports.register_module(path, [], [member]); } pub fn definition(&mut self, definition: &'a TypedDefinition) -> Option> { match definition { Definition::TypeAlias(TypeAlias { .. }) => None, // Handled in collect_imports Definition::Import(Import { .. }) => None, // Handled in collect_definitions Definition::CustomType(CustomType { .. }) => None, // If a definition is unused then we don't need to generate code for it Definition::ModuleConstant(ModuleConstant { location, .. }) | Definition::Function(Function { location, .. }) if self .module .unused_definition_positions .contains(&location.start) => { None } Definition::ModuleConstant(ModuleConstant { publicity, name, value, documentation, .. }) => Some(self.module_constant(*publicity, name, value, documentation)), Definition::Function(function) => { // 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; } self.module_function(function) } } } fn custom_type_definition( &mut self, constructors: &'a [TypedRecordConstructor], publicity: Publicity, opaque: bool, ) -> Vec> { // If there's no constructors then there's nothing to do here. if constructors.is_empty() { return vec![]; } self.tracker.custom_type_used = true; constructors .iter() .map(|constructor| Ok(self.record_definition(constructor, publicity, opaque))) .collect() } fn record_definition( &self, constructor: &'a TypedRecordConstructor, publicity: Publicity, opaque: bool, ) -> Document<'a> { fn parameter((i, arg): (usize, &TypedRecordConstructorArg)) -> Document<'_> { arg.label .as_ref() .map(|(_, s)| maybe_escape_identifier(s)) .unwrap_or_else(|| eco_format!("${i}")) .to_doc() } let doc = if let Some((_, documentation)) = &constructor.documentation { jsdoc_comment(documentation, publicity).append(line()) } else { nil() }; let head = if publicity.is_private() || opaque { "class " } else { "export class " }; let head = docvec![head, &constructor.name, " extends $CustomType {"]; if constructor.arguments.is_empty() { return head.append("}"); }; let parameters = join( constructor.arguments.iter().enumerate().map(parameter), break_(",", ", "), ); let constructor_body = join( constructor.arguments.iter().enumerate().map(|(i, arg)| { let var = parameter((i, arg)); match &arg.label { None => docvec!["this[", i, "] = ", var, ";"], Some((_, name)) => { docvec!["this.", maybe_escape_property(name), " = ", var, ";"] } } }), line(), ); let class_body = docvec![ line(), "constructor(", parameters, ") {", docvec![line(), "super();", line(), constructor_body].nest(INDENT), line(), "}", ] .nest(INDENT); docvec![doc, head, class_body, line(), "}"] } fn collect_definitions(&mut self) -> Vec> { self.module .definitions .iter() .flat_map(|definition| match definition { // If a custom type is unused then we don't need to generate code for it Definition::CustomType(CustomType { location, .. }) if self .module .unused_definition_positions .contains(&location.start) => { vec![] } Definition::CustomType(CustomType { publicity, constructors, opaque, .. }) => self.custom_type_definition(constructors, *publicity, *opaque), Definition::Function(Function { .. }) | Definition::TypeAlias(TypeAlias { .. }) | Definition::Import(Import { .. }) | Definition::ModuleConstant(ModuleConstant { .. }) => vec![], }) .collect() } fn collect_imports(&mut self) -> Imports<'a> { let mut imports = Imports::new(); for definition in &self.module.definitions { match definition { Definition::Import(Import { module, as_name, unqualified_values: unqualified, package, .. }) => { self.register_import(&mut imports, package, module, as_name, unqualified); } Definition::Function(Function { name: Some((_, name)), publicity, external_javascript: Some((module, function, _location)), .. }) => { self.register_external_function( &mut imports, *publicity, name, module, function, ); } Definition::Function(Function { .. }) | Definition::TypeAlias(TypeAlias { .. }) | Definition::CustomType(CustomType { .. }) | Definition::ModuleConstant(ModuleConstant { .. }) => (), } } imports } fn import_path(&self, package: &'a str, module: &'a 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, imports: &mut Imports<'a>, 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() }); let name = maybe_escape_identifier(&i.name).to_doc(); 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, imports: &mut Imports<'a>, publicity: Publicity, name: &'a str, module: &'a str, fun: &'a str, ) { let needs_escaping = !is_usable_js_identifier(name); let member = Member { name: fun.to_doc(), alias: if name == fun && !needs_escaping { None } else if needs_escaping { Some(escape_identifier(name).to_doc()) } else { Some(name.to_doc()) }, }; if publicity.is_importable() { imports.register_export(maybe_escape_identifier_string(name)) } imports.register_module(EcoString::from(module), [], [member]); } fn module_constant( &mut self, publicity: Publicity, name: &'a EcoString, value: &'a TypedConstant, documentation: &'a Option<(u32, EcoString)>, ) -> Output<'a> { let head = if publicity.is_private() { "const " } else { "export const " }; 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(), ); let document = generator.constant_expression(Context::Constant, value)?; let jsdoc = if let Some((_, documentation)) = documentation { jsdoc_comment(documentation, publicity).append(line()) } else { nil() }; Ok(docvec![ jsdoc, head, maybe_escape_identifier(name), " = ", document, ";", ]) } fn register_in_scope(&mut self, name: &str) { let _ = self.module_scope.insert(name.into(), 0); } fn module_function(&mut self, function: &'a TypedFunction) -> Option> { 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 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(), ); let function_doc = match &function.documentation { None => nil(), Some((_, documentation)) => { jsdoc_comment(documentation, function.publicity).append(line()) } }; let head = if function.publicity.is_private() { "function " } else { "export function " }; let body = match generator.function_body(&function.body, function.arguments.as_slice()) { // No error, let's continue! Ok(body) => body, // There is an error coming from some expression that is not supported on JavaScript // and the target support is not enforced. In this case we do not error, instead // returning nothing which will cause no function to be generated. Err(error) if error.is_unsupported() && !self.target_support.is_enforced() => { return None; } // Some other error case which will be returned to the user. Err(error) => return Some(Err(error)), }; let document = docvec![ function_doc, head, maybe_escape_identifier(name.as_str()), fun_args(function.arguments.as_slice(), generator.tail_recursion_used), " {", docvec![line(), body].nest(INDENT).group(), line(), "}", ]; Some(Ok(document)) } fn register_module_definitions_in_scope(&mut self) { for definition in self.module.definitions.iter() { match definition { Definition::ModuleConstant(ModuleConstant { name, .. }) => { self.register_in_scope(name) } Definition::Function(Function { name, .. }) => self.register_in_scope( name.as_ref() .map(|(_, name)| name) .expect("Function in a definition must be named"), ), Definition::Import(Import { unqualified_values: unqualified, .. }) => unqualified .iter() .for_each(|unq_import| self.register_in_scope(unq_import.used_name())), Definition::TypeAlias(TypeAlias { .. }) | Definition::CustomType(CustomType { .. }) => (), } } } fn filepath_definition(&self) -> Document<'a> { if !self.tracker.make_error_used { return nil(); } docvec!["const FILEPATH = ", self.src_path.clone(), ';', lines(2)] } } fn jsdoc_comment(documentation: &EcoString, publicity: Publicity) -> Document<'_> { let doc_lines = documentation .trim_end() .split('\n') .map(|line| eco_format!(" *{line}").to_doc()) .collect_vec(); // We start with the documentation of the function let doc_body = join(doc_lines, line()); let mut doc = docvec!["/**", line(), doc_body, line()]; 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![doc, " * ", line(), " * @ignore", line()]; } // And finally we close the doc comment docvec![doc, " */"] } #[derive(Debug)] pub struct ModuleConfig<'a> { pub module: &'a TypedModule, pub line_numbers: &'a LineNumbers, pub src: &'a EcoString, pub target_support: TargetSupport, pub typescript: TypeScriptDeclarations, pub stdlib_package: StdlibPackage, pub path: &'a Utf8Path, pub project_root: &'a Utf8Path, } pub fn module(config: ModuleConfig<'_>) -> Result { let path = config.path.to_path_buf(); let src = config.src.clone(); let document = Generator::new(config) .compile() .map_err(|error| crate::Error::JavaScript { path, src, error })?; Ok(document.to_pretty_string(80)) } pub fn ts_declaration( module: &TypedModule, path: &Utf8Path, src: &EcoString, ) -> Result { let document = typescript::TypeScriptGenerator::new(module) .compile() .map_err(|error| crate::Error::JavaScript { path: path.to_path_buf(), src: src.clone(), error, })?; Ok(document.to_pretty_string(80)) } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Error { Unsupported { feature: String, location: SrcSpan }, } impl Error { /// Returns `true` if the error is [`Unsupported`]. /// /// [`Unsupported`]: Error::Unsupported #[must_use] pub fn is_unsupported(&self) -> bool { matches!(self, Self::Unsupported { .. }) } } fn fun_args(args: &'_ [TypedArg], tail_recursion_used: bool) -> Document<'_> { let mut discards = 0; wrap_args(args.iter().map(|a| match a.get_variable_name() { None => { let doc = if discards == 0 { "_".to_doc() } else { eco_format!("_{discards}").to_doc() }; discards += 1; doc } Some(name) if tail_recursion_used => eco_format!("loop${name}").to_doc(), Some(name) => maybe_escape_identifier(name).to_doc(), })) } fn wrap_args<'a, I>(args: I) -> Document<'a> where I: IntoIterator>, { break_("", "") .append(join(args, break_(",", ", "))) .nest(INDENT) .append(break_("", "")) .surround("(", ")") .group() } fn wrap_object<'a>( items: impl IntoIterator, Option>)>, ) -> Document<'a> { let mut empty = true; let fields = items.into_iter().map(|(key, value)| { empty = false; match value { Some(value) => docvec![key, ": ", value], None => key.to_doc(), } }); let fields = join(fields, break_(",", ", ")); if empty { "{}".to_doc() } else { docvec![ docvec!["{", break_("", " "), fields] .nest(INDENT) .append(break_("", " ")) .group(), "}" ] } } 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. | "prototpye" | "__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_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, } fn bool(bool: bool) -> Document<'static> { match bool { true => "true".to_doc(), false => "false".to_doc(), } } /// 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, ) -> Result, Error> { // 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(); } Ok(bytes) }