Fork of daniellemaywood.uk/gleam — Wasm codegen work
24 kB
685 lines
1use std::{
2 collections::{HashMap, HashSet},
3 ops::Deref,
4};
5
6use ecow::{EcoString, eco_format};
7use itertools::Itertools;
8
9use crate::{
10 ast::{
11 ArgNames, CustomType, Definition, Function, ModuleConstant, Publicity,
12 RecordConstructorArg, SrcSpan, TypeAlias, TypedArg, TypedDefinition,
13 TypedRecordConstructor,
14 },
15 docvec,
16 pretty::{Document, Documentable, break_, join, line, nil, zero_width_string},
17 type_::{
18 Deprecation, PRELUDE_MODULE_NAME, PRELUDE_PACKAGE_NAME, Type, TypeVar, printer::Names,
19 },
20};
21
22use super::{
23 Dependency, DependencyKind, DocsValues, TypeConstructor, TypeConstructorArg, TypeDefinition,
24 markdown_documentation, source_links::SourceLinker, text_documentation,
25};
26
27#[derive(Clone, Copy)]
28pub struct PrintOptions {
29 pub print_highlighting: bool,
30 pub print_html: bool,
31}
32
33impl PrintOptions {
34 pub fn all() -> Self {
35 Self {
36 print_highlighting: true,
37 print_html: true,
38 }
39 }
40}
41
42pub struct Printer<'a> {
43 options: PrintOptions,
44 names: &'a Names,
45
46 package: EcoString,
47 module: EcoString,
48
49 /// Type variables which don't have annotated names and we have generated
50 /// names for.
51 printed_type_variables: HashMap<u64, EcoString>,
52 /// Names of type variables that we have generated or printed in a given
53 /// definition. This ensures that we have no duplicate generated names.
54 printed_type_variable_names: HashSet<EcoString>,
55 /// An incrementing number used to generate the next type variable name.
56 /// `0` becomes `a`, `1` becomes `b`, etc.
57 next_type_variable_id: u64,
58
59 dependencies: &'a HashMap<EcoString, Dependency>,
60}
61
62impl Printer<'_> {
63 pub fn new<'a>(
64 package: EcoString,
65 module: EcoString,
66 names: &'a Names,
67 dependencies: &'a HashMap<EcoString, Dependency>,
68 ) -> Printer<'a> {
69 Printer {
70 options: PrintOptions::all(),
71 names,
72 package,
73 module,
74 printed_type_variables: HashMap::new(),
75 printed_type_variable_names: HashSet::new(),
76 next_type_variable_id: 0,
77 dependencies,
78 }
79 }
80
81 // This is currently only used in the tests, though it might be useful in
82 // application code in future. If it is needed, simply remove this attribute.
83 #[cfg(test)]
84 pub fn set_options(&mut self, options: PrintOptions) {
85 self.options = options;
86 }
87
88 pub fn type_definition<'a>(
89 &mut self,
90 source_links: &SourceLinker,
91 statement: &'a TypedDefinition,
92 ) -> Option<TypeDefinition<'a>> {
93 match statement {
94 Definition::CustomType(CustomType {
95 publicity: Publicity::Public,
96 location,
97 name,
98 constructors,
99 documentation,
100 deprecation,
101 opaque,
102 parameters,
103 ..
104 }) => Some(TypeDefinition {
105 name,
106 definition: print(self.custom_type(name, parameters, constructors, *opaque)),
107 documentation: markdown_documentation(documentation),
108 text_documentation: text_documentation(documentation),
109 deprecation_message: match deprecation {
110 Deprecation::NotDeprecated => "".to_string(),
111 Deprecation::Deprecated { message } => message.to_string(),
112 },
113 constructors: if *opaque {
114 Vec::new()
115 } else {
116 constructors
117 .iter()
118 .map(|constructor| TypeConstructor {
119 definition: print(self.record_constructor(constructor)),
120 documentation: markdown_documentation(&constructor.documentation),
121 text_documentation: text_documentation(&constructor.documentation),
122 arguments: constructor
123 .arguments
124 .iter()
125 .filter_map(|arg| arg.label.as_ref().map(|(_, label)| (arg, label)))
126 .map(|(argument, label)| TypeConstructorArg {
127 name: label.trim_end().to_string(),
128 doc: markdown_documentation(&argument.doc),
129 })
130 .filter(|arg| !arg.doc.is_empty())
131 .collect(),
132 })
133 .collect()
134 },
135 source_url: source_links.url(*location),
136 opaque: *opaque,
137 }),
138
139 Definition::TypeAlias(TypeAlias {
140 publicity: Publicity::Public,
141 location,
142 alias: name,
143 parameters,
144 type_,
145 documentation,
146 deprecation,
147 ..
148 }) => Some(TypeDefinition {
149 name,
150 definition: print(self.type_alias(name, type_, parameters).group()),
151 documentation: markdown_documentation(documentation),
152 text_documentation: text_documentation(documentation),
153 constructors: vec![],
154 source_url: source_links.url(*location),
155 deprecation_message: match deprecation {
156 Deprecation::NotDeprecated => "".to_string(),
157 Deprecation::Deprecated { message } => message.to_string(),
158 },
159 opaque: false,
160 }),
161
162 Definition::TypeAlias(_)
163 | Definition::CustomType(_)
164 | Definition::Function(_)
165 | Definition::Import(_)
166 | Definition::ModuleConstant(_) => None,
167 }
168 }
169
170 pub fn value<'a>(
171 &mut self,
172 source_links: &SourceLinker,
173 statement: &'a TypedDefinition,
174 ) -> Option<DocsValues<'a>> {
175 // Ensure that any type variables we printed in previous definitions don't
176 // affect our printing of this definition. Two type variables in different
177 // definitions can have the same name without clashing.
178 self.printed_type_variable_names.clear();
179 self.next_type_variable_id = 0;
180
181 match statement {
182 Definition::Function(Function {
183 publicity: Publicity::Public,
184 name: Some((_, name)),
185 documentation: doc,
186 location,
187 deprecation,
188 arguments,
189 return_type,
190 ..
191 }) => Some(DocsValues {
192 name,
193 definition: print(self.function_signature(name, arguments, return_type)),
194 documentation: markdown_documentation(doc),
195 text_documentation: text_documentation(doc),
196 source_url: source_links.url(*location),
197 deprecation_message: match deprecation {
198 Deprecation::NotDeprecated => "".to_string(),
199 Deprecation::Deprecated { message } => message.to_string(),
200 },
201 }),
202
203 Definition::ModuleConstant(ModuleConstant {
204 publicity: Publicity::Public,
205 documentation,
206 location,
207 name,
208 type_,
209 deprecation,
210 ..
211 }) => Some(DocsValues {
212 name,
213 definition: print(self.constant(name, type_)),
214 documentation: markdown_documentation(documentation),
215 text_documentation: text_documentation(documentation),
216 source_url: source_links.url(*location),
217 deprecation_message: match deprecation {
218 Deprecation::NotDeprecated => "".to_string(),
219 Deprecation::Deprecated { message } => message.to_string(),
220 },
221 }),
222
223 Definition::TypeAlias(_)
224 | Definition::CustomType(_)
225 | Definition::Function(_)
226 | Definition::Import(_)
227 | Definition::ModuleConstant(_) => None,
228 }
229 }
230
231 fn custom_type<'a>(
232 &mut self,
233 name: &'a str,
234 parameters: &'a [(SrcSpan, EcoString)],
235 constructors: &'a [TypedRecordConstructor],
236 opaque: bool,
237 ) -> Document<'a> {
238 let arguments = if parameters.is_empty() {
239 nil()
240 } else {
241 Self::wrap_arguments(
242 parameters
243 .iter()
244 .map(|(_, parameter)| self.variable(parameter)),
245 )
246 };
247
248 let keywords = if opaque {
249 "pub opaque type "
250 } else {
251 "pub type "
252 };
253
254 let type_head = docvec![self.keyword(keywords), self.title(name), arguments];
255
256 if constructors.is_empty() || opaque {
257 return type_head;
258 }
259
260 let constructors = constructors
261 .iter()
262 .map(|constructor| {
263 line()
264 .append(self.record_constructor(constructor))
265 .nest(INDENT)
266 })
267 .collect_vec();
268
269 docvec![type_head, " {", constructors, line(), "}"]
270 }
271
272 pub fn record_constructor<'a>(
273 &mut self,
274 constructor: &'a TypedRecordConstructor,
275 ) -> Document<'a> {
276 if constructor.arguments.is_empty() {
277 return self.title(&constructor.name);
278 }
279
280 let arguments = constructor.arguments.iter().map(
281 |RecordConstructorArg { label, type_, .. }| match label {
282 Some((_, label)) => self.variable(label).append(": ").append(self.type_(type_)),
283 None => self.type_(type_),
284 },
285 );
286
287 let arguments = Self::wrap_arguments(arguments);
288
289 docvec![self.title(&constructor.name), arguments].group()
290 }
291
292 fn type_alias<'a>(
293 &mut self,
294 name: &'a str,
295 type_: &Type,
296 parameters: &[(SrcSpan, EcoString)],
297 ) -> Document<'a> {
298 let parameters = if parameters.is_empty() {
299 nil()
300 } else {
301 let arguments = parameters
302 .iter()
303 .map(|(_, parameter)| self.variable(parameter));
304 Self::wrap_arguments(arguments)
305 };
306
307 docvec![
308 self.keyword("pub type "),
309 self.title(name),
310 parameters,
311 " =",
312 line().append(self.type_(type_)).nest(INDENT)
313 ]
314 }
315
316 fn constant<'a>(&mut self, name: &'a str, type_: &Type) -> Document<'a> {
317 self.register_local_type_variable_names(type_);
318
319 docvec![
320 self.keyword("pub const "),
321 self.title(name),
322 ": ",
323 self.type_(type_)
324 ]
325 }
326
327 fn function_signature<'a>(
328 &mut self,
329 name: &'a str,
330 arguments: &'a [TypedArg],
331 return_type: &Type,
332 ) -> Document<'a> {
333 for argument in arguments {
334 self.register_local_type_variable_names(&argument.type_);
335 }
336 self.register_local_type_variable_names(return_type);
337
338 let arguments = if arguments.is_empty() {
339 "()".to_doc()
340 } else {
341 Self::wrap_arguments(arguments.iter().map(|argument| {
342 let name = self.variable(self.argument_name(argument));
343 docvec![name, ": ", self.type_(&argument.type_)].group()
344 }))
345 };
346
347 docvec![
348 self.keyword("pub fn "),
349 self.title(name),
350 arguments,
351 " -> ",
352 self.type_(return_type)
353 ]
354 .group()
355 }
356
357 fn argument_name<'a>(&self, arg: &'a TypedArg) -> Document<'a> {
358 match &arg.names {
359 ArgNames::Named { name, .. } => name.to_doc(),
360 ArgNames::NamedLabelled { label, name, .. } => docvec![label, " ", name],
361 // We remove the underscore from discarded function arguments since we don't want to
362 // expose this kind of detail: https://github.com/gleam-lang/gleam/issues/2561
363 ArgNames::Discard { name, .. } => match name.strip_prefix('_').unwrap_or(name) {
364 "" => "arg".to_doc(),
365 name => name.to_doc(),
366 },
367 ArgNames::LabelledDiscard { label, name, .. } => {
368 docvec![label, " ", name.strip_prefix('_').unwrap_or(name).to_doc()]
369 }
370 }
371 }
372
373 fn wrap_arguments<'a>(arguments: impl IntoIterator<Item = Document<'a>>) -> Document<'a> {
374 break_("(", "(")
375 .append(join(arguments, break_(",", ", ")))
376 .nest_if_broken(INDENT)
377 .append(break_(",", ""))
378 .append(")")
379 }
380
381 fn type_arguments<'a>(arguments: impl IntoIterator<Item = Document<'a>>) -> Document<'a> {
382 break_("", "")
383 .append(join(arguments, break_(",", ", ")))
384 .nest_if_broken(INDENT)
385 .append(break_(",", ""))
386 .group()
387 .surround("(", ")")
388 }
389
390 fn type_(&mut self, type_: &Type) -> Document<'static> {
391 match type_ {
392 Type::Named {
393 package,
394 module,
395 name,
396 args,
397 publicity,
398 ..
399 } => {
400 let name = self.named_type_name(publicity, package, module, name);
401 if args.is_empty() {
402 name
403 } else {
404 name.append(Self::type_arguments(
405 args.iter().map(|argument| self.type_(argument)),
406 ))
407 }
408 }
409 Type::Fn { args, return_ } => docvec![
410 self.keyword("fn"),
411 Self::type_arguments(args.iter().map(|argument| self.type_(argument))),
412 " -> ",
413 self.type_(return_)
414 ],
415 Type::Tuple { elements } => docvec![
416 "#",
417 Self::type_arguments(elements.iter().map(|element| self.type_(element))),
418 ],
419 Type::Var { type_ } => match type_.as_ref().borrow().deref() {
420 TypeVar::Link { type_ } => self.type_(type_),
421
422 TypeVar::Unbound { id } | TypeVar::Generic { id } => {
423 let name = self.type_variable(*id);
424 self.variable(name)
425 }
426 },
427 }
428 }
429
430 fn type_variable(&mut self, id: u64) -> EcoString {
431 if let Some(name) = self.names.get_type_variable(id) {
432 return name.clone();
433 }
434
435 if let Some(name) = self.printed_type_variables.get(&id) {
436 return name.clone();
437 }
438
439 loop {
440 let name = self.next_letter();
441 if !self.printed_type_variable_names.contains(&name) {
442 _ = self.printed_type_variable_names.insert(name.clone());
443 _ = self.printed_type_variables.insert(id, name.clone());
444 return name;
445 }
446 }
447 }
448
449 // Copied from the `next_letter` method of the `type_::printer`.
450 fn next_letter(&mut self) -> EcoString {
451 let alphabet_length = 26;
452 let char_offset = b'a';
453 let mut chars = vec![];
454 let mut n;
455 let mut rest = self.next_type_variable_id;
456
457 loop {
458 n = rest % alphabet_length;
459 rest = rest / alphabet_length;
460 chars.push((n as u8 + char_offset) as char);
461
462 if rest == 0 {
463 break;
464 }
465 rest -= 1
466 }
467
468 self.next_type_variable_id += 1;
469 chars.into_iter().rev().collect()
470 }
471
472 fn named_type_name(
473 &self,
474 publicity: &Publicity,
475 package: &str,
476 module: &str,
477 name: &EcoString,
478 ) -> Document<'static> {
479 // There's no documentation page for the prelude
480 if package == PRELUDE_PACKAGE_NAME && module == PRELUDE_MODULE_NAME {
481 return self.title(name);
482 }
483
484 // Internal types don't get linked
485 if !publicity.is_public() {
486 return docvec![self.comment("@internal ".to_doc()), self.title(name)];
487 }
488
489 // Linking to a type within the same page
490 if package == self.package && module == self.module {
491 return self.link(eco_format!("#{name}"), self.title(name), None);
492 }
493
494 // Linking to a module within the package
495 if package == self.package {
496 // If we are linking to the current package, we might be viewing the
497 // documentation locally and so we need to generate a relative link.
498
499 let mut module_path = module.split('/').peekable();
500 let mut current_module = self.module.split('/');
501
502 // The documentation page for the final segment of the module is just
503 // an html file by itself, so it doesn't form part of the path and doesn't
504 // need to be backtracked using `..`.
505 let module_name = module_path.next_back().unwrap_or(module);
506 _ = current_module.next_back();
507
508 // The two modules might have some sharer part of the path, which we
509 // don't need to traverse back through. However, if the two modules are
510 // something like `gleam/a/wibble/wobble` and `gleam/b/wibble/wobble`,
511 // the `wibble` folders are two different folders despite being at the
512 // same position with the same name.
513 let mut encountered_different_path = false;
514 let mut path = Vec::new();
515
516 // Calculate how far backwards in the directory tree we need to walk
517 for segment in current_module {
518 // If this is still part of the shared path, we can just skip it:
519 // no need to go back and forth through the same directory in the
520 // path!
521 if !encountered_different_path && module_path.peek() == Some(&segment) {
522 _ = module_path.next();
523 } else {
524 encountered_different_path = true;
525 path.push("..");
526 }
527 }
528
529 // Once we have walked backwards, we walk forwards again to the correct
530 // page.
531 path.extend(module_path);
532 path.push(module_name);
533
534 let qualified_name = docvec![
535 self.variable(EcoString::from(module_name)),
536 ".",
537 self.title(name)
538 ];
539
540 let title = eco_format!("{module}.{{type {name}}}");
541
542 return self.link(
543 eco_format!("{path}.html#{name}", path = path.join("/")),
544 qualified_name,
545 Some(title),
546 );
547 }
548
549 let module_name = module.split('/').next_back().unwrap_or(module);
550 let qualified_name = docvec![
551 self.variable(EcoString::from(module_name)),
552 ".",
553 self.title(name)
554 ];
555 let title = eco_format!("{module}.{{type {name}}}");
556
557 // We can't reliably link to documentation if the type is from a path
558 // or git dependency
559 match self.dependencies.get(package) {
560 Some(Dependency {
561 kind: DependencyKind::Hex,
562 version,
563 }) => self.link(
564 eco_format!("https://hexdocs.pm/{package}/{version}/{module}.html#{name}"),
565 qualified_name,
566 Some(title),
567 ),
568 Some(_) | None => self.span_with_title(qualified_name, title),
569 }
570 }
571
572 /// Walk a type and register all the type variable names which occur within
573 /// it. This is to ensure that when generating type variable names for
574 /// unannotated arguments, we don't print any that clash with existing names.
575 ///
576 /// We preregister all names before actually printing anything, because we
577 /// could run into code like this:
578 ///
579 /// ```gleam
580 /// pub fn wibble(_, _: a) -> b {}
581 /// ```
582 ///
583 /// If we did not preregister the type variables in this case, we would end
584 /// up printing `fn wibble(_: a, _: a) -> b` which is not correct.
585 ///
586 fn register_local_type_variable_names(&mut self, type_: &Type) {
587 match type_ {
588 Type::Named { args, .. } => {
589 for arg in args {
590 self.register_local_type_variable_names(arg);
591 }
592 }
593 Type::Fn { args, return_ } => {
594 for arg in args {
595 self.register_local_type_variable_names(arg);
596 }
597 self.register_local_type_variable_names(return_);
598 }
599 Type::Var { type_ } => match type_.borrow().deref() {
600 TypeVar::Link { type_ } => self.register_local_type_variable_names(type_),
601 TypeVar::Unbound { id } | TypeVar::Generic { id } => {
602 if let Some(name) = self.names.get_type_variable(*id) {
603 _ = self.printed_type_variable_names.insert(name.clone());
604 }
605 }
606 },
607 Type::Tuple { elements } => {
608 for element in elements {
609 self.register_local_type_variable_names(element);
610 }
611 }
612 }
613 }
614
615 fn keyword<'a>(&self, keyword: impl Documentable<'a>) -> Document<'a> {
616 self.colour_span(keyword, "keyword")
617 }
618
619 fn comment<'a>(&self, name: impl Documentable<'a>) -> Document<'a> {
620 self.colour_span(name, "comment")
621 }
622
623 fn title<'a>(&self, name: impl Documentable<'a>) -> Document<'a> {
624 self.colour_span(name, "title")
625 }
626
627 fn variable<'a>(&self, name: impl Documentable<'a>) -> Document<'a> {
628 self.colour_span(name, "variable")
629 }
630
631 fn colour_span<'a>(
632 &self,
633 name: impl Documentable<'a>,
634 colour_class: &'static str,
635 ) -> Document<'a> {
636 if !self.options.print_highlighting {
637 return name.to_doc();
638 }
639
640 name.to_doc().surround(
641 zero_width_string(eco_format!(r#"<span class="hljs-{colour_class}">"#)),
642 zero_width_string("</span>".into()),
643 )
644 }
645
646 fn link<'a>(
647 &self,
648 href: EcoString,
649 name: impl Documentable<'a>,
650 title: Option<EcoString>,
651 ) -> Document<'a> {
652 if !self.options.print_html {
653 return name.to_doc();
654 }
655
656 let opening_tag = if let Some(title) = title {
657 eco_format!(r#"<a href="{href}" title="{title}">"#)
658 } else {
659 eco_format!(r#"<a href="{href}">"#)
660 };
661
662 name.to_doc().surround(
663 zero_width_string(opening_tag),
664 zero_width_string("</a>".into()),
665 )
666 }
667
668 fn span_with_title<'a>(&self, name: impl Documentable<'a>, title: EcoString) -> Document<'a> {
669 if !self.options.print_html {
670 return name.to_doc();
671 }
672
673 name.to_doc().surround(
674 zero_width_string(eco_format!(r#"<span title="{title}">"#)),
675 zero_width_string("</span>".into()),
676 )
677 }
678}
679
680const MAX_COLUMNS: isize = 65;
681const INDENT: isize = 2;
682
683fn print(doc: Document<'_>) -> String {
684 doc.to_pretty_string(MAX_COLUMNS)
685}