Fork of daniellemaywood.uk/gleam — Wasm codegen work
Something went wrong. Try again.
31 kB · 899 lines
Rust
at wasm
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900// SPDX-License-Identifier: Apache-2.0// SPDX-FileCopyrightText: 2025 The Gleam contributors
use std::{ collections::{HashMap, HashSet}, ops::Deref,};
use ecow::{EcoString, eco_format};
use crate::{ ast::{ ArgNames, CustomType, Function, Publicity, RecordConstructorArg, SrcSpan, TypeAlias, TypedArg, TypedDefinitions, TypedModuleConstant, TypedRecordConstructor, }, type_::{ Deprecation, PRELUDE_MODULE_NAME, PRELUDE_PACKAGE_NAME, Type, TypeVar, printer::{Names, PrintMode}, },};use pretty_arena::*;
use super::{ Dependency, DependencyKind, DocsValues, TypeConstructor, TypeConstructorArg, TypeDefinition, markdown_documentation, source_links::SourceLinker, text_documentation,};
#[derive(Clone, Copy)]pub struct PrintOptions { pub print_highlighting: bool, pub print_html: bool,}
impl PrintOptions { pub fn all() -> Self { Self { print_highlighting: true, print_html: true, } }}
pub struct Printer<'a> { options: PrintOptions, names: &'a Names,
package: EcoString, module: EcoString,
/// Type variables which don't have annotated names and we have generated /// names for. printed_type_variables: HashMap<u64, EcoString>, /// Names of type variables that we have generated or printed in a given /// definition. This ensures that we have no duplicate generated names. printed_type_variable_names: HashSet<EcoString>, /// An incrementing number used to generate the next type variable name. /// `0` becomes `a`, `1` becomes `b`, etc. next_type_variable_id: u64,
dependencies: &'a HashMap<EcoString, Dependency>,}
impl<'a, 'doc> Printer<'a> { pub fn new( package: EcoString, module: EcoString, names: &'a Names, dependencies: &'a HashMap<EcoString, Dependency>, ) -> Printer<'a> { Printer { options: PrintOptions::all(), names, package, module, printed_type_variables: HashMap::new(), printed_type_variable_names: HashSet::new(), next_type_variable_id: 0, dependencies, } }
// This is currently only used in the tests, though it might be useful in // application code in future. If it is needed, simply remove this attribute. #[cfg(test)] pub fn set_options(&mut self, options: PrintOptions) { self.options = options; }
pub fn type_definitions( &mut self, source_links: &SourceLinker, definitions: &'a TypedDefinitions, ) -> Vec<TypeDefinition<'a>> { let mut type_definitions = vec![]; let arena = DocumentArena::new();
for CustomType { location, name, publicity, constructors, documentation, deprecation, opaque, parameters, .. } in &definitions.custom_types { if !publicity.is_public() { continue; }
type_definitions.push(TypeDefinition { name, definition: print(self.custom_type( &arena, name, parameters, constructors, *opaque, )), raw_definition: self .raw(|this| this.custom_type(&arena, name, parameters, constructors, *opaque)), documentation: markdown_documentation(documentation), text_documentation: text_documentation(documentation), deprecation_message: match deprecation { Deprecation::NotDeprecated => "".to_string(), Deprecation::Deprecated { message } => message.to_string(), }, constructors: if *opaque { Vec::new() } else { constructors .iter() .map(|constructor| TypeConstructor { definition: print(self.record_constructor(&arena, constructor)), raw_definition: self .raw(|this| this.record_constructor(&arena, constructor)), documentation: markdown_documentation(&constructor.documentation), text_documentation: text_documentation(&constructor.documentation), arguments: constructor .arguments .iter() .filter_map(|arg| arg.label.as_ref().map(|(_, label)| (arg, label))) .map(|(argument, label)| TypeConstructorArg { name: label.trim_end().to_string(), doc: markdown_documentation(&argument.doc), text_documentation: text_documentation(&argument.doc), }) .filter(|arg| !arg.doc.is_empty()) .collect(), }) .collect() }, source_url: source_links.url(*location), opaque: *opaque, }); }
for TypeAlias { location, alias: name, parameters, type_, publicity, documentation, deprecation, .. } in &definitions.type_aliases { if !publicity.is_public() { continue; } type_definitions.push(TypeDefinition { name, definition: print( self.type_alias(&arena, name, type_, parameters) .group(&arena), ), raw_definition: self.raw(|this| { this.type_alias(&arena, name, type_, parameters) .group(&arena) }), documentation: markdown_documentation(documentation), text_documentation: text_documentation(documentation), constructors: vec![], source_url: source_links.url(*location), deprecation_message: match deprecation { Deprecation::NotDeprecated => "".to_string(), Deprecation::Deprecated { message } => message.to_string(), }, opaque: false, }); }
type_definitions.sort(); type_definitions }
/// Print a definition without HTML highlighting, such as for search data fn raw<'doc1, 'a1: 'doc1>( &mut self, definition: impl FnOnce(&mut Self) -> Document<'a1, 'doc1>, ) -> String { let options = self.options; // Turn off highlighting for this definition self.options = PrintOptions { print_highlighting: false, print_html: false, }; let result = print(definition(self)); // Restore previous options self.options = options; format!("```\n{result}\n```") }
pub fn value_definitions( &mut self, source_links: &SourceLinker, definitions: &'a TypedDefinitions, ) -> Vec<DocsValues<'a>> { let mut value_definitions = vec![]; let arena = DocumentArena::new();
for Function { location, name, arguments, publicity, deprecation, return_type, documentation, .. } in &definitions.functions { let Some((_, name)) = name else { continue }; if !publicity.is_public() { continue; }
// Ensure that any type variables we printed in previous definitions don't // affect our printing of this definition. Two type variables in different // definitions can have the same name without clashing. self.printed_type_variable_names.clear(); self.next_type_variable_id = 0;
value_definitions.push(DocsValues { name, definition: print(self.function_signature(&arena, name, arguments, return_type)), raw_definition: self .raw(|this| this.function_signature(&arena, name, arguments, return_type)), documentation: markdown_documentation(documentation), text_documentation: text_documentation(documentation), source_url: source_links.url(*location), deprecation_message: match deprecation { Deprecation::NotDeprecated => "".to_string(), Deprecation::Deprecated { message } => message.to_string(), }, }); }
for TypedModuleConstant { documentation, location, publicity, name, type_, deprecation, .. } in &definitions.constants { if !publicity.is_public() { continue; }
value_definitions.push(DocsValues { name, definition: print(self.constant(&arena, name, type_)), raw_definition: self.raw(|this| this.constant(&arena, name, type_)), documentation: markdown_documentation(documentation), text_documentation: text_documentation(documentation), source_url: source_links.url(*location), deprecation_message: match deprecation { Deprecation::NotDeprecated => "".to_string(), Deprecation::Deprecated { message } => message.to_string(), }, }); }
value_definitions.sort(); value_definitions }
fn custom_type( &mut self, arena: &'doc DocumentArena<'a, 'doc>, name: &'a str, parameters: &'a [(SrcSpan, EcoString)], constructors: &'a [TypedRecordConstructor], opaque: bool, ) -> Document<'a, 'doc> { let arguments = if parameters.is_empty() { EMPTY_DOCUMENT } else { Self::wrap_arguments( arena, parameters .iter() .map(|(_, parameter)| self.variable(arena, parameter)), ) };
let type_head = docvec![ arena, self.keyword( arena, if opaque { PUB_OPAQUE_TYPE_SPACE_DOCUMENT } else { PUB_TYPE_SPACE_DOCUMENT } ), self.title(arena, name), arguments ];
if constructors.is_empty() || opaque { return type_head; }
let constructors = arena.concat(constructors.iter().map(|constructor| { LINE_DOCUMENT .append(arena, self.record_constructor(arena, constructor)) .nest(arena, INDENT) }));
docvec![ arena, type_head, SPACE_OPEN_CURLY_DOCUMENT, constructors, LINE_DOCUMENT, CLOSE_CURLY_DOCUMENT ] }
pub fn record_constructor( &mut self, arena: &'doc DocumentArena<'a, 'doc>, constructor: &'a TypedRecordConstructor, ) -> Document<'a, 'doc> { if constructor.arguments.is_empty() { return self.title(arena, &constructor.name); }
let arguments = constructor.arguments.iter().map( |RecordConstructorArg { label, type_, .. }| match label { Some((_, label)) => self .variable(arena, label) .append(arena, COLON_SPACE_DOCUMENT) .append(arena, self.type_(arena, type_, PrintMode::Normal)), None => self.type_(arena, type_, PrintMode::Normal), }, );
let arguments = Self::wrap_arguments(arena, arguments);
docvec![arena, self.title(arena, &constructor.name), arguments].group(arena) }
fn type_alias( &mut self, arena: &'doc DocumentArena<'a, 'doc>, name: &'a str, type_: &Type, parameters: &[(SrcSpan, EcoString)], ) -> Document<'a, 'doc> { let parameters = if parameters.is_empty() { EMPTY_DOCUMENT } else { let arguments = parameters .iter() .map(|(_, parameter)| self.variable(arena, parameter)); Self::wrap_arguments(arena, arguments) };
docvec![ arena, self.keyword(arena, PUB_TYPE_SPACE_DOCUMENT), self.title(arena, name), parameters, SPACE_EQUAL_DOCUMENT, LINE_DOCUMENT .append(arena, self.type_(arena, type_, PrintMode::ExpandAliases)) .nest(arena, INDENT) ] }
fn constant( &mut self, arena: &'doc DocumentArena<'a, 'doc>, name: &'a str, type_: &Type, ) -> Document<'a, 'doc> { self.register_local_type_variable_names(type_);
docvec![ arena, self.keyword(arena, PUB_CONST_SPACE_DOCUMENT), self.title(arena, name), COLON_SPACE_DOCUMENT, self.type_(arena, type_, PrintMode::Normal) ] }
fn function_signature( &mut self, arena: &'doc DocumentArena<'a, 'doc>, name: &'a str, arguments: &'a [TypedArg], return_type: &Type, ) -> Document<'a, 'doc> { for argument in arguments { self.register_local_type_variable_names(&argument.type_); } self.register_local_type_variable_names(return_type);
let arguments = if arguments.is_empty() { OPEN_CLOSE_PAREN_DOCUMENT } else { Self::wrap_arguments( arena, arguments.iter().map(|argument| { let name = self.variable(arena, self.argument_name(arena, argument)); docvec![ arena, name, COLON_SPACE_DOCUMENT, self.type_(arena, &argument.type_, PrintMode::Normal) ] .group(arena) }), ) };
docvec![ arena, self.keyword(arena, PUB_FN_SPACE_DOCUMENT), self.title(arena, name), arguments, SPACE_RIGHT_ARROW_SPACE_DOCUMENT, self.type_(arena, return_type, PrintMode::Normal) ] .group(arena) }
fn argument_name( &self, arena: &'doc DocumentArena<'a, 'doc>, arg: &'a TypedArg, ) -> Document<'a, 'doc> { match &arg.names { ArgNames::Named { name, .. } => name.to_doc(arena), ArgNames::NamedLabelled { label, name, .. } => { docvec![arena, label, SPACE_DOCUMENT, name] } // We remove the underscore from discarded function arguments since we don't want to // expose this kind of detail: https://github.com/gleam-lang/gleam/issues/2561 ArgNames::Discard { name, .. } => match name.strip_prefix('_').unwrap_or(name) { "" => LOWERCASE_ARG_DOCUMENT, name => name.to_doc(arena), }, ArgNames::LabelledDiscard { label, name, .. } => { docvec![ arena, label, SPACE_DOCUMENT, name.strip_prefix('_').unwrap_or(name).to_doc(arena) ] } } }
fn wrap_arguments( arena: &'doc DocumentArena<'a, 'doc>, arguments: impl IntoIterator<Item = Document<'a, 'doc>>, ) -> Document<'a, 'doc> { OPEN_PAREN_BREAK_DOCUMENT .append(arena, arena.join(arguments, COMMA_BREAK_DOCUMENT)) .nest_if_broken(arena, INDENT) .append(arena, TRAILING_COMMA_BREAK_DOCUMENT) .append(arena, CLOSE_PAREN_DOCUMENT) }
fn type_arguments( 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_if_broken(arena, INDENT) .append(arena, TRAILING_COMMA_BREAK_DOCUMENT) .group(arena) .surround(arena, OPEN_PAREN_DOCUMENT, CLOSE_PAREN_DOCUMENT) }
fn type_( &mut self, arena: &'doc DocumentArena<'a, 'doc>, type_: &Type, print_mode: PrintMode, ) -> Document<'a, 'doc> { match type_ { Type::Named { package, module, name, arguments, publicity, .. } => { let name = match print_mode { // If we are printing a type for a type alias, and the alias // is reexporting an internal type, we want to show that it // is aliasing that internal type, rather than showing it as // aliasing itself. PrintMode::ExpandAliases if *package == self.package => { self.named_type_name(arena, publicity, package, module, name) } // If we are printing a type alias which aliases an internal // type from a different package, we still want to print the // public name for that type. If we are not printing a type // alias at all, we also want to use the public name. PrintMode::ExpandAliases | PrintMode::Normal => { // If we are using a reexported internal type, we want to // print it public name, whether it is from this package // or otherwise. if let Some((module, alias)) = self.names.reexport_alias(module.clone(), name.clone()) { self.named_type_name(arena, &Publicity::Public, package, module, alias) } else { self.named_type_name(arena, publicity, package, module, name) } } };
if arguments.is_empty() { name } else { name.append( arena, Self::type_arguments( arena, arguments .iter() .map(|argument| self.type_(arena, argument, PrintMode::Normal)), ), ) } } Type::Fn { arguments, return_ } => docvec![ arena, self.keyword(arena, FN_DOCUMENT), Self::type_arguments( arena, arguments .iter() .map(|argument| self.type_(arena, argument, PrintMode::Normal)) ), SPACE_RIGHT_ARROW_SPACE_DOCUMENT, self.type_(arena, return_, PrintMode::Normal) ], Type::Tuple { elements } => docvec![ arena, HASHTAG_DOCUMENT, Self::type_arguments( arena, elements .iter() .map(|element| self.type_(arena, element, PrintMode::Normal)) ), ], Type::Var { type_ } => match type_.as_ref().borrow().deref() { TypeVar::Link { type_ } => self.type_(arena, type_, PrintMode::Normal),
TypeVar::Unbound { id } | TypeVar::Generic { id } => { let name = self.type_variable(*id); self.variable(arena, name) } }, } }
fn type_variable(&mut self, id: u64) -> EcoString { if let Some(name) = self.names.get_type_variable(id) { return name.clone(); }
if let Some(name) = self.printed_type_variables.get(&id) { return name.clone(); }
loop { let name = self.next_letter(); if !self.printed_type_variable_names.contains(&name) { _ = self.printed_type_variable_names.insert(name.clone()); _ = self.printed_type_variables.insert(id, name.clone()); return name; } } }
// Copied from the `next_letter` method of the `type_::printer`. fn next_letter(&mut self) -> EcoString { let alphabet_length = 26; let char_offset = b'a'; let mut chars = vec![]; let mut n; let mut rest = self.next_type_variable_id;
loop { n = rest % alphabet_length; rest = rest / alphabet_length; chars.push((n as u8 + char_offset) as char);
if rest == 0 { break; } rest -= 1; }
self.next_type_variable_id += 1; chars.into_iter().rev().collect() }
fn named_type_name( &self, arena: &'doc DocumentArena<'a, 'doc>, publicity: &Publicity, package: &str, module: &str, name: &EcoString, ) -> Document<'a, 'doc> { // There's no documentation page for the prelude if package == PRELUDE_PACKAGE_NAME && module == PRELUDE_MODULE_NAME { return self.title(arena, name); }
// Internal types don't get linked if !publicity.is_public() { return docvec![ arena, self.comment(arena, INTERNAL_ATTRIBUTE_SPACE_DOCUMENT), self.title(arena, name) ]; }
// Linking to a type within the same page if package == self.package && module == self.module { return self.link(arena, eco_format!("#{name}"), self.title(arena, name), None); }
// Linking to a module within the package if package == self.package { // If we are linking to the current package, we might be viewing the // documentation locally and so we need to generate a relative link.
let mut module_path = module.split('/').peekable(); let mut current_module = self.module.split('/');
// The documentation page for the final segment of the module is just // an html file by itself, so it doesn't form part of the path and doesn't // need to be backtracked using `..`. let module_name = module_path.next_back().unwrap_or(module); _ = current_module.next_back();
// The two modules might have some sharer part of the path, which we // don't need to traverse back through. However, if the two modules are // something like `gleam/a/wibble/wobble` and `gleam/b/wibble/wobble`, // the `wibble` folders are two different folders despite being at the // same position with the same name. let mut encountered_different_path = false; let mut path = Vec::new();
// Calculate how far backwards in the directory tree we need to walk for segment in current_module { // If this is still part of the shared path, we can just skip it: // no need to go back and forth through the same directory in the // path! if !encountered_different_path && module_path.peek() == Some(&segment) { _ = module_path.next(); } else { encountered_different_path = true; path.push(".."); } }
// Once we have walked backwards, we walk forwards again to the correct // page. path.extend(module_path); path.push(module_name);
let qualified_name = docvec![ arena, self.variable(arena, EcoString::from(module_name)), DOT_DOCUMENT, self.title(arena, name) ];
let title = eco_format!("{module}.{{type {name}}}");
return self.link( arena, eco_format!("{path}.html#{name}", path = path.join("/")), qualified_name, Some(title), ); }
let module_name = module.split('/').next_back().unwrap_or(module); let qualified_name = docvec![ arena, self.variable(arena, EcoString::from(module_name)), DOT_DOCUMENT, self.title(arena, name) ]; let title = eco_format!("{module}.{{type {name}}}");
// We can't reliably link to documentation if the type is from a path // or git dependency match self.dependencies.get(package) { Some(Dependency { kind: DependencyKind::Hex, version, }) => self.link( arena, eco_format!( "https://{package}.hexdocs.pm/{version}/{module}.html#{name}", package = package.replace('_', "-") ), qualified_name, Some(title), ), Some(_) | None => self.span_with_title(arena, qualified_name, title), } }
/// Walk a type and register all the type variable names which occur within /// it. This is to ensure that when generating type variable names for /// unannotated arguments, we don't print any that clash with existing names. /// /// We preregister all names before actually printing anything, because we /// could run into code like this: /// /// ```gleam /// pub fn wibble(_, _: a) -> b {} /// ``` /// /// If we did not preregister the type variables in this case, we would end /// up printing `fn wibble(_: a, _: a) -> b` which is not correct. /// fn register_local_type_variable_names(&mut self, type_: &Type) { match type_ { Type::Named { arguments, .. } => { for argument in arguments { self.register_local_type_variable_names(argument); } } Type::Fn { arguments, return_ } => { for argument in arguments { self.register_local_type_variable_names(argument); } self.register_local_type_variable_names(return_); } Type::Var { type_ } => match type_.borrow().deref() { TypeVar::Link { type_ } => self.register_local_type_variable_names(type_), TypeVar::Unbound { id } | TypeVar::Generic { id } => { if let Some(name) = self.names.get_type_variable(*id) { _ = self.printed_type_variable_names.insert(name.clone()); } } }, Type::Tuple { elements } => { for element in elements { self.register_local_type_variable_names(element); } } } }
fn keyword( &self, arena: &'doc DocumentArena<'a, 'doc>, keyword: Document<'static, 'static>, ) -> Document<'a, 'doc> { self.colour_span(arena, keyword, ColourClass::Keyword) }
fn comment( &self, arena: &'doc DocumentArena<'a, 'doc>, name: impl Documentable<'a, 'doc>, ) -> Document<'a, 'doc> { self.colour_span(arena, name, ColourClass::Comment) }
fn title( &self, arena: &'doc DocumentArena<'a, 'doc>, name: impl Documentable<'a, 'doc>, ) -> Document<'a, 'doc> { self.colour_span(arena, name, ColourClass::Title) }
fn variable( &self, arena: &'doc DocumentArena<'a, 'doc>, name: impl Documentable<'a, 'doc>, ) -> Document<'a, 'doc> { self.colour_span(arena, name, ColourClass::Variable) }
fn colour_span( &self, arena: &'doc DocumentArena<'a, 'doc>, name: impl Documentable<'a, 'doc>, colour_class: ColourClass, ) -> Document<'a, 'doc> { if !self.options.print_highlighting { return name.to_doc(arena); }
let open_tag = match colour_class { ColourClass::Variable => ZERO_WIDTH_OPEN_VARIABLE_COLOUR_SPAN, ColourClass::Title => ZERO_WIDTH_OPEN_TITLE_COLOUR_SPAN, ColourClass::Keyword => ZERO_WIDTH_OPEN_KEYWORD_COLOUR_SPAN, ColourClass::Comment => ZERO_WIDTH_OPEN_COMMENT_COLOUR_SPAN, };
name.to_doc(arena) .surround(arena, open_tag, ZERO_WIDTH_CLOSED_SPAN_TAG_DOCUMENT) }
fn link( &self, arena: &'doc DocumentArena<'a, 'doc>, href: EcoString, name: impl Documentable<'a, 'doc>, title: Option<EcoString>, ) -> Document<'a, 'doc> { if !self.options.print_html { return name.to_doc(arena); }
let opening_tag = if let Some(title) = title { eco_format!(r#"<a href="{href}" title="{title}">"#) } else { eco_format!(r#"<a href="{href}">"#) };
name.to_doc(arena).surround( arena, arena.zero_width_string(opening_tag), ZERO_WIDTH_CLOSED_A_TAG_DOCUMENT, ) }
fn span_with_title( &self, arena: &'doc DocumentArena<'a, 'doc>, name: impl Documentable<'a, 'doc>, title: EcoString, ) -> Document<'a, 'doc> { if !self.options.print_html { return name.to_doc(arena); }
name.to_doc(arena).surround( arena, arena.zero_width_string(eco_format!(r#"<span title="{title}">"#)), ZERO_WIDTH_CLOSED_SPAN_TAG_DOCUMENT, ) }}
enum ColourClass { Variable, Title, Keyword, Comment,}
const MAX_COLUMNS: isize = 65;const INDENT: isize = 2;
fn print<'a, 'doc>(doc: Document<'a, 'doc>) -> String { doc.to_pretty_string(MAX_COLUMNS)}