Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / compiler-core / src / docs / printer.rs
20 kB 584 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 DocsValues, TypeConstructor, TypeConstructorArg, TypeDefinition, markdown_documentation, 24 source_links::SourceLinker, text_documentation, 25}; 26 27#[derive(Clone, Copy)] 28pub struct PrintOptions { 29 pub print_highlighting: bool, 30 pub print_links: bool, 31} 32 33impl PrintOptions { 34 pub fn all() -> Self { 35 Self { 36 print_highlighting: true, 37 print_links: 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 60impl Printer<'_> { 61 pub fn new(package: EcoString, module: EcoString, names: &Names) -> Printer<'_> { 62 Printer { 63 options: PrintOptions::all(), 64 names, 65 package, 66 module, 67 printed_type_variables: HashMap::new(), 68 printed_type_variable_names: HashSet::new(), 69 next_type_variable_id: 0, 70 } 71 } 72 73 // This is currently only used in the tests, though it might be useful in 74 // application code in future. If it is needed, simply remove this attribute. 75 #[cfg(test)] 76 pub fn set_options(&mut self, options: PrintOptions) { 77 self.options = options; 78 } 79 80 pub fn type_definition<'a>( 81 &mut self, 82 source_links: &SourceLinker, 83 statement: &'a TypedDefinition, 84 ) -> Option<TypeDefinition<'a>> { 85 match statement { 86 Definition::CustomType(CustomType { 87 publicity: Publicity::Public, 88 location, 89 name, 90 constructors, 91 documentation, 92 deprecation, 93 opaque, 94 parameters, 95 .. 96 }) => Some(TypeDefinition { 97 name, 98 definition: print(self.custom_type(name, parameters, constructors, *opaque)), 99 documentation: markdown_documentation(documentation), 100 text_documentation: text_documentation(documentation), 101 deprecation_message: match deprecation { 102 Deprecation::NotDeprecated => "".to_string(), 103 Deprecation::Deprecated { message } => message.to_string(), 104 }, 105 constructors: if *opaque { 106 Vec::new() 107 } else { 108 constructors 109 .iter() 110 .map(|constructor| TypeConstructor { 111 definition: print(self.record_constructor(constructor)), 112 documentation: markdown_documentation(&constructor.documentation), 113 text_documentation: text_documentation(&constructor.documentation), 114 arguments: constructor 115 .arguments 116 .iter() 117 .filter_map(|arg| arg.label.as_ref().map(|(_, label)| (arg, label))) 118 .map(|(argument, label)| TypeConstructorArg { 119 name: label.trim_end().to_string(), 120 doc: markdown_documentation(&argument.doc), 121 }) 122 .filter(|arg| !arg.doc.is_empty()) 123 .collect(), 124 }) 125 .collect() 126 }, 127 source_url: source_links.url(*location), 128 opaque: *opaque, 129 }), 130 Definition::TypeAlias(TypeAlias { 131 publicity: Publicity::Public, 132 location, 133 alias: name, 134 parameters, 135 type_, 136 documentation, 137 deprecation, 138 .. 139 }) => Some(TypeDefinition { 140 name, 141 definition: print(self.type_alias(name, type_, parameters).group()), 142 documentation: markdown_documentation(documentation), 143 text_documentation: text_documentation(documentation), 144 constructors: vec![], 145 source_url: source_links.url(*location), 146 deprecation_message: match deprecation { 147 Deprecation::NotDeprecated => "".to_string(), 148 Deprecation::Deprecated { message } => message.to_string(), 149 }, 150 opaque: false, 151 }), 152 Definition::TypeAlias(_) 153 | Definition::CustomType(_) 154 | Definition::Function(_) 155 | Definition::Import(_) 156 | Definition::ModuleConstant(_) => None, 157 } 158 } 159 160 pub fn value<'a>( 161 &mut self, 162 source_links: &SourceLinker, 163 statement: &'a TypedDefinition, 164 ) -> Option<DocsValues<'a>> { 165 // Ensure that any type variables we printed in previous definitions don't 166 // affect our printing of this definition. Two type variables in different 167 // definitions can have the same name without clashing. 168 self.printed_type_variable_names.clear(); 169 self.next_type_variable_id = 0; 170 171 match statement { 172 Definition::Function(Function { 173 publicity: Publicity::Public, 174 name: Some((_, name)), 175 documentation: doc, 176 location, 177 deprecation, 178 arguments, 179 return_type, 180 .. 181 }) => Some(DocsValues { 182 name, 183 definition: print(self.function_signature(name, arguments, return_type)), 184 documentation: markdown_documentation(doc), 185 text_documentation: text_documentation(doc), 186 source_url: source_links.url(*location), 187 deprecation_message: match deprecation { 188 Deprecation::NotDeprecated => "".to_string(), 189 Deprecation::Deprecated { message } => message.to_string(), 190 }, 191 }), 192 193 Definition::ModuleConstant(ModuleConstant { 194 publicity: Publicity::Public, 195 documentation, 196 location, 197 name, 198 type_, 199 deprecation, 200 .. 201 }) => Some(DocsValues { 202 name, 203 definition: print(self.constant(name, type_)), 204 documentation: markdown_documentation(documentation), 205 text_documentation: text_documentation(documentation), 206 source_url: source_links.url(*location), 207 deprecation_message: match deprecation { 208 Deprecation::NotDeprecated => "".to_string(), 209 Deprecation::Deprecated { message } => message.to_string(), 210 }, 211 }), 212 213 Definition::TypeAlias(_) 214 | Definition::CustomType(_) 215 | Definition::Function(_) 216 | Definition::Import(_) 217 | Definition::ModuleConstant(_) => None, 218 } 219 } 220 221 fn custom_type<'a>( 222 &mut self, 223 name: &'a str, 224 parameters: &'a [(SrcSpan, EcoString)], 225 constructors: &'a [TypedRecordConstructor], 226 opaque: bool, 227 ) -> Document<'a> { 228 let arguments = if parameters.is_empty() { 229 nil() 230 } else { 231 Self::wrap_arguments( 232 parameters 233 .iter() 234 .map(|(_, parameter)| self.variable(parameter)), 235 ) 236 }; 237 238 let keywords = if opaque { 239 "pub opaque type " 240 } else { 241 "pub type " 242 }; 243 244 let type_head = docvec![self.keyword(keywords), self.title(name), arguments]; 245 246 if constructors.is_empty() || opaque { 247 return type_head; 248 } 249 250 let constructors = constructors 251 .iter() 252 .map(|constructor| { 253 line() 254 .append(self.record_constructor(constructor)) 255 .nest(INDENT) 256 }) 257 .collect_vec(); 258 259 docvec![type_head, " {", constructors, line(), "}"] 260 } 261 262 pub fn record_constructor<'a>( 263 &mut self, 264 constructor: &'a TypedRecordConstructor, 265 ) -> Document<'a> { 266 if constructor.arguments.is_empty() { 267 return self.title(&constructor.name); 268 } 269 270 let arguments = constructor.arguments.iter().map( 271 |RecordConstructorArg { label, type_, .. }| match label { 272 Some((_, label)) => self.variable(label).append(": ").append(self.type_(type_)), 273 None => self.type_(type_), 274 }, 275 ); 276 277 let arguments = Self::wrap_arguments(arguments); 278 279 docvec![self.title(&constructor.name), arguments] 280 } 281 282 fn type_alias<'a>( 283 &mut self, 284 name: &'a str, 285 type_: &Type, 286 parameters: &[(SrcSpan, EcoString)], 287 ) -> Document<'a> { 288 let parameters = if parameters.is_empty() { 289 nil() 290 } else { 291 let arguments = parameters 292 .iter() 293 .map(|(_, parameter)| self.variable(parameter)); 294 Self::wrap_arguments(arguments) 295 }; 296 297 docvec![ 298 self.keyword("pub type "), 299 self.title(name), 300 parameters, 301 " =", 302 line().append(self.type_(type_)).nest(INDENT) 303 ] 304 } 305 306 fn constant<'a>(&mut self, name: &'a str, type_: &Type) -> Document<'a> { 307 self.register_local_type_variable_names(type_); 308 309 docvec![ 310 self.keyword("pub const "), 311 self.title(name), 312 ": ", 313 self.type_(type_) 314 ] 315 } 316 317 fn function_signature<'a>( 318 &mut self, 319 name: &'a str, 320 arguments: &'a [TypedArg], 321 return_type: &Type, 322 ) -> Document<'a> { 323 for argument in arguments { 324 self.register_local_type_variable_names(&argument.type_); 325 } 326 self.register_local_type_variable_names(return_type); 327 328 let arguments = arguments.iter().map(|argument| { 329 let name = self.variable(self.argument_name(argument)); 330 docvec![name, ": ", self.type_(&argument.type_)].group() 331 }); 332 let arguments = Self::wrap_arguments(arguments); 333 334 docvec![ 335 self.keyword("pub fn "), 336 self.title(name), 337 arguments, 338 " -> ", 339 self.type_(return_type) 340 ] 341 .group() 342 } 343 344 fn argument_name<'a>(&self, arg: &'a TypedArg) -> Document<'a> { 345 match &arg.names { 346 ArgNames::Named { name, .. } => name.to_doc(), 347 ArgNames::NamedLabelled { label, name, .. } => docvec![label, " ", name], 348 // We remove the underscore from discarded function arguments since we don't want to 349 // expose this kind of detail: https://github.com/gleam-lang/gleam/issues/2561 350 ArgNames::Discard { name, .. } => match name.strip_prefix('_').unwrap_or(name) { 351 "" => "arg".to_doc(), 352 name => name.to_doc(), 353 }, 354 ArgNames::LabelledDiscard { label, name, .. } => { 355 docvec![label, " ", name.strip_prefix('_').unwrap_or(name).to_doc()] 356 } 357 } 358 } 359 360 fn wrap_arguments<'a>(arguments: impl IntoIterator<Item = Document<'a>>) -> Document<'a> { 361 break_("(", "(") 362 .append(join(arguments, break_(",", ", "))) 363 .nest_if_broken(INDENT) 364 .append(break_(",", "")) 365 .append(")") 366 } 367 368 fn type_arguments<'a>(arguments: impl IntoIterator<Item = Document<'a>>) -> Document<'a> { 369 break_("", "") 370 .append(join(arguments, break_(",", ", "))) 371 .nest_if_broken(INDENT) 372 .append(break_(",", "")) 373 .group() 374 .surround("(", ")") 375 } 376 377 fn type_(&mut self, type_: &Type) -> Document<'static> { 378 match type_ { 379 Type::Named { 380 package, 381 module, 382 name, 383 args, 384 .. 385 } => { 386 let name = self.named_type_name(package, module, name); 387 if args.is_empty() { 388 name 389 } else { 390 name.append(Self::type_arguments( 391 args.iter().map(|argument| self.type_(argument)), 392 )) 393 } 394 } 395 Type::Fn { args, return_ } => docvec![ 396 self.keyword("fn"), 397 Self::type_arguments(args.iter().map(|argument| self.type_(argument))), 398 " -> ", 399 self.type_(return_) 400 ], 401 Type::Tuple { elements } => docvec![ 402 "#", 403 Self::type_arguments(elements.iter().map(|element| self.type_(element))), 404 ], 405 Type::Var { type_ } => match type_.as_ref().borrow().deref() { 406 TypeVar::Link { type_ } => self.type_(type_), 407 408 TypeVar::Unbound { id } | TypeVar::Generic { id } => { 409 let name = self.type_variable(*id); 410 self.variable(name) 411 } 412 }, 413 } 414 } 415 416 fn type_variable(&mut self, id: u64) -> EcoString { 417 if let Some(name) = self.names.get_type_variable(id) { 418 return name.clone(); 419 } 420 421 if let Some(name) = self.printed_type_variables.get(&id) { 422 return name.clone(); 423 } 424 425 loop { 426 let name = self.next_letter(); 427 if !self.printed_type_variable_names.contains(&name) { 428 _ = self.printed_type_variable_names.insert(name.clone()); 429 _ = self.printed_type_variables.insert(id, name.clone()); 430 return name; 431 } 432 } 433 } 434 435 // Copied from the `next_letter` method of the `type_::printer`. 436 fn next_letter(&mut self) -> EcoString { 437 let alphabet_length = 26; 438 let char_offset = b'a'; 439 let mut chars = vec![]; 440 let mut n; 441 let mut rest = self.next_type_variable_id; 442 443 loop { 444 n = rest % alphabet_length; 445 rest = rest / alphabet_length; 446 chars.push((n as u8 + char_offset) as char); 447 448 if rest == 0 { 449 break; 450 } 451 rest -= 1 452 } 453 454 self.next_type_variable_id += 1; 455 chars.into_iter().rev().collect() 456 } 457 458 fn named_type_name(&self, package: &str, module: &str, name: &EcoString) -> Document<'static> { 459 if package == PRELUDE_PACKAGE_NAME && module == PRELUDE_MODULE_NAME { 460 self.title(name) 461 } else if package == self.package && module == self.module { 462 self.link(eco_format!("#{name}"), self.title(name), None) 463 } else { 464 let module_name = module.split('/').next_back().unwrap_or(module); 465 let qualified_name = docvec![ 466 self.variable(EcoString::from(module_name)), 467 ".", 468 self.title(name) 469 ]; 470 let title = eco_format!("{module}.{{type {name}}}"); 471 472 self.link( 473 eco_format!("https://hexdocs.pm/{package}/{module}.html#{name}"), 474 qualified_name, 475 Some(title), 476 ) 477 } 478 } 479 480 /// Walk a type and register all the type variable names which occur within 481 /// it. This is to ensure that when generating type variable names for 482 /// unannotated arguments, we don't print any that clash with existing names. 483 /// 484 /// We preregister all names before actually printing anything, because we 485 /// could run into code like this: 486 /// 487 /// ```gleam 488 /// pub fn wibble(_, _: a) -> b {} 489 /// ``` 490 /// 491 /// If we did not preregister the type variables in this case, we would end 492 /// up printing `fn wibble(_: a, _: a) -> b` which is not correct. 493 /// 494 fn register_local_type_variable_names(&mut self, type_: &Type) { 495 match type_ { 496 Type::Named { args, .. } => { 497 for arg in args { 498 self.register_local_type_variable_names(arg); 499 } 500 } 501 Type::Fn { args, return_ } => { 502 for arg in args { 503 self.register_local_type_variable_names(arg); 504 } 505 self.register_local_type_variable_names(return_); 506 } 507 Type::Var { type_ } => match type_.borrow().deref() { 508 TypeVar::Link { type_ } => self.register_local_type_variable_names(type_), 509 TypeVar::Unbound { id } | TypeVar::Generic { id } => { 510 if let Some(name) = self.names.get_type_variable(*id) { 511 _ = self.printed_type_variable_names.insert(name.clone()); 512 } 513 } 514 }, 515 Type::Tuple { elements } => { 516 for element in elements { 517 self.register_local_type_variable_names(element); 518 } 519 } 520 } 521 } 522 523 fn keyword<'a>(&self, keyword: impl Documentable<'a>) -> Document<'a> { 524 if !self.options.print_highlighting { 525 return keyword.to_doc(); 526 } 527 528 keyword.to_doc().surround( 529 zero_width_string(r#"<span class="hljs-keyword">"#.into()), 530 zero_width_string("</span>".into()), 531 ) 532 } 533 534 fn title<'a>(&self, name: impl Documentable<'a>) -> Document<'a> { 535 if !self.options.print_highlighting { 536 return name.to_doc(); 537 } 538 539 name.to_doc().surround( 540 zero_width_string(r#"<span class="hljs-title">"#.into()), 541 zero_width_string("</span>".into()), 542 ) 543 } 544 545 fn variable<'a>(&self, name: impl Documentable<'a>) -> Document<'a> { 546 if !self.options.print_highlighting { 547 return name.to_doc(); 548 } 549 550 name.to_doc().surround( 551 zero_width_string(r#"<span class="hljs-variable">"#.into()), 552 zero_width_string("</span>".into()), 553 ) 554 } 555 556 fn link<'a>( 557 &self, 558 href: EcoString, 559 name: impl Documentable<'a>, 560 title: Option<EcoString>, 561 ) -> Document<'a> { 562 if !self.options.print_links { 563 return name.to_doc(); 564 } 565 566 let opening_tag = if let Some(title) = title { 567 eco_format!(r#"<a href="{href}" title="{title}">"#) 568 } else { 569 eco_format!(r#"<a href="{href}">"#) 570 }; 571 572 name.to_doc().surround( 573 zero_width_string(opening_tag), 574 zero_width_string("</a>".into()), 575 ) 576 } 577} 578 579const MAX_COLUMNS: isize = 65; 580const INDENT: isize = 2; 581 582fn print(doc: Document<'_>) -> String { 583 doc.to_pretty_string(MAX_COLUMNS) 584}