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 / pretty.rs
24 kB 659 lines
1//! This module implements the functionality described in 2//! ["Strictly Pretty" (2000) by Christian Lindig][0], with a few 3//! extensions. 4//! 5//! This module is heavily influenced by Elixir's Inspect.Algebra and 6//! JavaScript's Prettier. 7//! 8//! [0]: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.34.2200 9//! 10//! ## Extensions 11//! 12//! - `ForcedBreak` from Elixir. 13//! - `FlexBreak` from Elixir. 14#![allow(clippy::wrong_self_convention)] 15 16#[cfg(test)] 17mod tests; 18 19use ecow::EcoString; 20use itertools::Itertools; 21 22use crate::{io::Utf8Writer, Result}; 23 24#[macro_export] 25macro_rules! docvec { 26 () => { 27 Document::Vec(Vec::new()) 28 }; 29 30 ($($x:expr),+ $(,)?) => { 31 Document::Vec(vec![$($x.to_doc()),+]) 32 }; 33} 34 35/// Coerce a value into a Document. 36/// Note we do not implement this for String as a slight pressure to favour str 37/// over String. 38pub trait Documentable<'a> { 39 fn to_doc(self) -> Document<'a>; 40} 41 42impl<'a> Documentable<'a> for char { 43 fn to_doc(self) -> Document<'a> { 44 Document::String(format!("{self}")) 45 } 46} 47 48impl<'a> Documentable<'a> for &'a str { 49 fn to_doc(self) -> Document<'a> { 50 Document::Str(self) 51 } 52} 53 54impl<'a> Documentable<'a> for EcoString { 55 fn to_doc(self) -> Document<'a> { 56 Document::EcoString(self) 57 } 58} 59 60impl<'a> Documentable<'a> for &EcoString { 61 fn to_doc(self) -> Document<'a> { 62 Document::EcoString(self.clone()) 63 } 64} 65 66impl<'a> Documentable<'a> for isize { 67 fn to_doc(self) -> Document<'a> { 68 Document::String(format!("{self}")) 69 } 70} 71 72impl<'a> Documentable<'a> for i64 { 73 fn to_doc(self) -> Document<'a> { 74 Document::String(format!("{self}")) 75 } 76} 77 78impl<'a> Documentable<'a> for usize { 79 fn to_doc(self) -> Document<'a> { 80 Document::String(format!("{self}")) 81 } 82} 83 84impl<'a> Documentable<'a> for f64 { 85 fn to_doc(self) -> Document<'a> { 86 Document::String(format!("{self:?}")) 87 } 88} 89 90impl<'a> Documentable<'a> for u64 { 91 fn to_doc(self) -> Document<'a> { 92 Document::String(format!("{self:?}")) 93 } 94} 95 96impl<'a> Documentable<'a> for u32 { 97 fn to_doc(self) -> Document<'a> { 98 Document::String(format!("{self}")) 99 } 100} 101 102impl<'a> Documentable<'a> for u16 { 103 fn to_doc(self) -> Document<'a> { 104 Document::String(format!("{self}")) 105 } 106} 107 108impl<'a> Documentable<'a> for u8 { 109 fn to_doc(self) -> Document<'a> { 110 Document::String(format!("{self}")) 111 } 112} 113 114impl<'a> Documentable<'a> for Document<'a> { 115 fn to_doc(self) -> Document<'a> { 116 self 117 } 118} 119 120impl<'a> Documentable<'a> for Vec<Document<'a>> { 121 fn to_doc(self) -> Document<'a> { 122 Document::Vec(self) 123 } 124} 125 126impl<'a, D: Documentable<'a>> Documentable<'a> for Option<D> { 127 fn to_doc(self) -> Document<'a> { 128 self.map(Documentable::to_doc).unwrap_or_else(nil) 129 } 130} 131 132pub fn concat<'a>(docs: impl IntoIterator<Item = Document<'a>>) -> Document<'a> { 133 Document::Vec(docs.into_iter().collect()) 134} 135 136pub fn join<'a>( 137 docs: impl IntoIterator<Item = Document<'a>>, 138 separator: Document<'a>, 139) -> Document<'a> { 140 concat(Itertools::intersperse(docs.into_iter(), separator)) 141} 142 143#[derive(Debug, Clone, PartialEq, Eq)] 144pub enum Document<'a> { 145 /// A mandatory linebreak 146 Line(usize), 147 148 /// Forces the breaks of the wrapped document to be considered as not 149 /// fitting on a single line. Used in combination with a `Group` it can be 150 /// used to force its `Break`s to always break. 151 ForceBroken(Box<Self>), 152 153 /// Ignore the next break 154 NextBreakFits(Box<Self>, NextBreakFitsMode), 155 156 /// Renders `broken` if group is broken, `unbroken` otherwise 157 Break { 158 broken: &'a str, 159 unbroken: &'a str, 160 kind: BreakKind, 161 }, 162 163 /// Join multiple documents together 164 Vec(Vec<Self>), 165 166 /// Nests the given document by the given indent, depending on the specified 167 /// condition 168 Nest(isize, NestMode, NestCondition, Box<Self>), 169 170 /// Nests the given document to the current cursor position 171 Group(Box<Self>), 172 173 /// A string to render 174 String(String), 175 176 /// A str to render 177 Str(&'a str), 178 179 /// A string that is cheap to copy 180 EcoString(EcoString), 181} 182 183#[derive(Debug, Clone, Copy, PartialEq, Eq)] 184enum Mode { 185 /// The mode used when a group doesn't fit on a single line: when `Broken` 186 /// the `Break`s inside it will be rendered as newlines, splitting the 187 /// group. 188 Broken, 189 190 /// The default mode used when a group can fit on a single line: all its 191 /// `Break`s will be rendered as their unbroken string and kept on a single 192 /// line. 193 Unbroken, 194 195 /// This mode is used by the `NextBreakFit` document to force a break to be 196 /// considered as broken. 197 ForcedBroken, 198 199 /// This mode is used to disable a `NextBreakFit` document. 200 ForcedUnbroken, 201} 202 203/// A flag that can be used to enable or disable a `NextBreakFit` document. 204#[derive(Debug, Clone, Copy, PartialEq, Eq)] 205pub enum NextBreakFitsMode { 206 Enabled, 207 Disabled, 208} 209 210/// A flag that can be used to conditionally disable a `Nest` document. 211#[derive(Debug, Clone, Copy, PartialEq, Eq)] 212pub enum NestCondition { 213 /// This always applies the nesting. This is a sensible default that will 214 /// work for most of the cases. 215 Always, 216 /// Only applies the nesting if the wrapping `Group` couldn't fit on a 217 /// single line and has been broken. 218 IfBroken, 219} 220 221/// Used to change the way nesting of documents work. 222#[derive(Debug, Clone, Copy, PartialEq, Eq)] 223pub enum NestMode { 224 /// If the nesting mode is `Increase`, the current indentation will be 225 /// increased by the specified value. 226 Increase, 227 /// If the nesting mode is `Set`, the current indentation is going to be set 228 /// to exactly the specified value. 229 /// 230 /// `doc.nest(2).set_nesting(0)` 231 /// "foo 232 /// bar <- no indentation is added! 233 /// baz" 234 Set, 235} 236 237fn fits( 238 limit: isize, 239 mut current_width: isize, 240 mut docs: im::Vector<(isize, Mode, &Document<'_>)>, 241) -> bool { 242 // The `fits` function is going to take each document from the `docs` queue 243 // and check if those can fit on a single line. In order to do so documents 244 // are going to be pushed in front of this queue and have to be accompanied 245 // by additional information: 246 // - the document indentation, that can be increased by the `Nest` block. 247 // - the current mode; this is needed to know if a group is being broken or 248 // not and treat `Break`s differently as a consequence. You can see how 249 // the behaviour changes in [ref:break-fit]. 250 // 251 // The loop might be broken earlier without checking all documents under one 252 // of two conditions: 253 // - the documents exceed the line `limit` and surely won't fit 254 // [ref:document-unfit]. 255 // - the documents are sure to fit the line - for example, if we meet a 256 // broken `Break` [ref:break-fit] or a newline [ref:newline-fit]. 257 loop { 258 // [tag:document-unfit] If we've exceeded the maximum width allowed for 259 // a line, it means that the document won't fit on a single line, we can 260 // break the loop. 261 if current_width > limit { 262 return false; 263 }; 264 265 // We start by checking the first document of the queue. If there's no 266 // documents then we can safely say that it fits (if reached this point 267 // it means that the limit wasn't exceeded). 268 let (indent, mode, document) = match docs.pop_front() { 269 Some(x) => x, 270 None => return true, 271 }; 272 273 match document { 274 // If a document is marked as `ForceBroken` we can immediately say 275 // that it doesn't fit, so that every break is going to be 276 // forcefully broken. 277 Document::ForceBroken(doc) => match mode { 278 // If the mode is `ForcedBroken` it means that we have to ignore 279 // this break [ref:forced-broken], so we go check the inner 280 // document ignoring the effects of this one. 281 Mode::ForcedBroken => docs.push_front((indent, mode, doc)), 282 _ => return false, 283 }, 284 285 // [tag:newline-fit] When we run into a line we know that the 286 // document has a bit that fits in the current line; if it didn't 287 // fit (that is, it exceeded the maximum allowed width) the loop 288 // would have been broken by one of the earlier checks. 289 Document::Line(_) => return true, 290 291 // If the nesting level is increased we go on checking the wrapped 292 // document and increase its indentation level based on the nesting 293 // condition. 294 Document::Nest(i, nest_mode, condition, doc) => match condition { 295 NestCondition::IfBroken => docs.push_front((indent, mode, doc)), 296 NestCondition::Always => { 297 let new_indent = match nest_mode { 298 NestMode::Increase => indent + i, 299 NestMode::Set => *i, 300 }; 301 docs.push_front((new_indent, mode, doc)) 302 } 303 }, 304 305 // As a general rule, a group fits if it can stay on a single line 306 // without its breaks being broken down. 307 Document::Group(doc) => match mode { 308 // If an outer group was broken, we still try to fit the inner 309 // group on a single line, that's why for the inner document 310 // we change the mode back to `Unbroken`. 311 Mode::Broken => docs.push_front((indent, Mode::Unbroken, doc)), 312 // Any other mode is preserved as-is: if the mode is forced it 313 // has to be left unchanged, and if the mode is already unbroken 314 // there's no need to change it. 315 _ => docs.push_front((indent, mode, doc)), 316 }, 317 318 // When we run into a string we increase the current_width; looping 319 // back we will check if we've exceeded the maximum allowed width. 320 Document::Str(s) => current_width += s.len() as isize, 321 Document::String(s) => current_width += s.len() as isize, 322 Document::EcoString(s) => current_width += s.len() as isize, 323 324 // If we get to a break we need to first see if it has to be 325 // rendered as its unbroken or broken string, depending on the mode. 326 Document::Break { unbroken, .. } => match mode { 327 // [tag:break-fit] If the break has to be broken we're done! 328 // We haven't exceeded the maximum length (otherwise the loop 329 // iteration would have stopped with one of the earlier checks), 330 // and - since it needs to be broken - we'll have to go on a new 331 // line anyway. 332 // This means that the document inspected so far will fit on a 333 // single line, thus we return true. 334 Mode::Broken | Mode::ForcedBroken => return true, 335 // If the break is not broken then it will be rendered inline as 336 // its unbroken string, so we treat it exactly as if it were a 337 // normal string. 338 Mode::Unbroken | Mode::ForcedUnbroken => current_width += unbroken.len() as isize, 339 }, 340 341 // The `NextBreakFits` can alter the current mode to `ForcedBroken` 342 // or `ForcedUnbroken` based on its enabled flag. 343 Document::NextBreakFits(doc, enabled) => match enabled { 344 // [tag:disable-next-break] If it is disabled then we check the 345 // wrapped document changing the mode to `ForcedUnbroken`. 346 NextBreakFitsMode::Disabled => docs.push_front((indent, Mode::ForcedUnbroken, doc)), 347 NextBreakFitsMode::Enabled => match mode { 348 // If we're in `ForcedUnbroken` mode it means that the check 349 // was disabled by a document wrapping this one 350 // [ref:disable-next-break]; that's why we do nothing and 351 // check the wrapped document as if it were a normal one. 352 Mode::ForcedUnbroken => docs.push_front((indent, mode, doc)), 353 // [tag:forced-broken] Any other mode is turned into 354 // `ForcedBroken` so that when we run into a break, the 355 // response to the question "Does the document fit?" will be 356 // yes [ref:break-fit]. 357 // This is why this is called `NextBreakFit` I think. 358 _ => docs.push_front((indent, Mode::ForcedBroken, doc)), 359 }, 360 }, 361 362 // If there's a sequence of documents we will check each one, one 363 // after the other to see if - as a whole - they can fit on a single 364 // line. 365 Document::Vec(vec) => { 366 // The array needs to be reversed to preserve the order of the 367 // documents since each one is pushed _to the front_ of the 368 // queue of documents to check. 369 for doc in vec.iter().rev() { 370 docs.push_front((indent, mode, doc)); 371 } 372 } 373 } 374 } 375} 376 377#[derive(Debug, Clone, Copy, PartialEq, Eq)] 378pub enum BreakKind { 379 Flex, 380 Strict, 381} 382 383fn format( 384 writer: &mut impl Utf8Writer, 385 limit: isize, 386 mut width: isize, 387 mut docs: im::Vector<(isize, Mode, &Document<'_>)>, 388) -> Result<()> { 389 // As long as there are documents to print we'll take each one by one and 390 // output the corresponding string to the given writer. 391 // 392 // Each document in the `docs` queue also has an accompanying indentation 393 // and mode: 394 // - the indentation is used to keep track of the current indentation, 395 // you might notice in [ref:format-nest] that it adds documents to the 396 // queue increasing their current indentation. 397 // - the mode is used to keep track of the state of the documents inside a 398 // group. For example, if a group doesn't fit on a single line its 399 // documents will be split into multiple lines and the mode set to 400 // `Broken` to keep track of this. 401 while let Some((indent, mode, document)) = docs.pop_front() { 402 match document { 403 // When we run into a line we print the given number of newlines and 404 // add the indentation required by the given document. 405 Document::Line(i) => { 406 for _ in 0..*i { 407 writer.str_write("\n")?; 408 } 409 for _ in 0..indent { 410 writer.str_write(" ")?; 411 } 412 width = indent; 413 } 414 415 // Flex breaks are NOT conditional to the mode: if the mode is 416 // already `Unbroken`, then the break is left unbroken (like strict 417 // breaks); any other mode is ignored. 418 // A flexible break will only be split if the following documents 419 // can't fit on the same line; otherwise, it is just displayed as an 420 // unbroken `Break`. 421 Document::Break { 422 broken, 423 unbroken, 424 kind: BreakKind::Flex, 425 } => { 426 let unbroken_width = width + unbroken.len() as isize; 427 // Every time we need to check again if the remaining piece can 428 // fit. If it does, the flexible break is not broken. 429 if mode == Mode::Unbroken || fits(limit, unbroken_width, docs.clone()) { 430 writer.str_write(unbroken)?; 431 width = unbroken_width; 432 } else { 433 writer.str_write(broken)?; 434 writer.str_write("\n")?; 435 for _ in 0..indent { 436 writer.str_write(" ")?; 437 } 438 width = indent; 439 } 440 } 441 442 // Strict breaks are conditional to the mode. They differ from 443 // flexible break because, if a group gets split - that is the mode 444 // is `Broken` or `ForceBroken` - ALL of the breaks in that group 445 // will be split. You can notice the difference with flexible breaks 446 // because here we only check the mode and then take action; before 447 // we would try and see if the remaining documents fit on a single 448 // line before deciding if the (flexible) break can be split or not. 449 Document::Break { 450 broken, 451 unbroken, 452 kind: BreakKind::Strict, 453 } => match mode { 454 // If the mode requires the break to be broken, then its broken 455 // string is printed, then we start a newline and indent it 456 // according to the current indentation level. 457 Mode::Broken | Mode::ForcedBroken => { 458 writer.str_write(broken)?; 459 writer.str_write("\n")?; 460 for _ in 0..indent { 461 writer.str_write(" ")?; 462 } 463 width = indent; 464 } 465 // If the mode doesn't require the break to be broken, then its 466 // unbroken string is printed as if it were a normal string; 467 // also updating the width of the current line. 468 Mode::Unbroken | Mode::ForcedUnbroken => { 469 writer.str_write(unbroken)?; 470 width += unbroken.len() as isize 471 } 472 }, 473 474 // Strings are printed as they are and the current width is 475 // increased accordingly. 476 Document::String(s) => { 477 width += s.len() as isize; 478 writer.str_write(s)?; 479 } 480 481 Document::EcoString(s) => { 482 width += s.len() as isize; 483 writer.str_write(s)?; 484 } 485 486 Document::Str(s) => { 487 width += s.len() as isize; 488 writer.str_write(s)?; 489 } 490 491 // If multiple documents need to be printed, then they are all 492 // pushed to the front of the queue and will be printed one by one. 493 Document::Vec(vec) => { 494 // Just like `fits`, the elements will be pushed _on the front_ 495 // of the queue. In order to keep their original order they need 496 // to be pushed in reverse order. 497 for doc in vec.iter().rev() { 498 docs.push_front((indent, mode, doc)); 499 } 500 } 501 502 // A `Nest` document doesn't result in anything being printed, its 503 // only effect is to increase the current nesting level for the 504 // wrapped document [tag:format-nest]. 505 Document::Nest(i, nest_mode, condition, doc) => match (condition, mode) { 506 // The nesting is only applied under two conditions: 507 // - either the nesting condition is `Always`. 508 // - or the condition is `IfBroken` and the group was actually 509 // broken (that is, the current mode is `Broken`). 510 (NestCondition::Always, _) | (NestCondition::IfBroken, Mode::Broken) => { 511 let new_indent = match nest_mode { 512 NestMode::Increase => indent + i, 513 NestMode::Set => *i, 514 }; 515 docs.push_front((new_indent, mode, doc)) 516 } 517 // If none of the above conditions is met, then the nesting is 518 // not applied. 519 _ => docs.push_front((indent, mode, doc)), 520 }, 521 522 Document::Group(doc) => { 523 // When we see a group we first try and see if it can fit on a 524 // single line without breaking any break; that is why we use 525 // the `Unbroken` mode here: we want to try to fit everything on 526 // a single line. 527 let group_docs = im::vector![(indent, Mode::Unbroken, doc.as_ref())]; 528 if fits(limit, width, group_docs) { 529 // If everything can stay on a single line we print the 530 // wrapped document with the `Unbroken` mode, leaving all 531 // the group's break as unbroken. 532 docs.push_front((indent, Mode::Unbroken, doc)); 533 } else { 534 // Otherwise, we need to break the group. We print the 535 // wrapped document changing its mode to `Broken` so that 536 // all its breaks will be split on newlines. 537 docs.push_front((indent, Mode::Broken, doc)); 538 } 539 } 540 541 // `ForceBroken` and `NextBreakFits` only change the way the `fit` 542 // function works but do not actually change the formatting of a 543 // document by themselves. That's why when we run into those we 544 // just go on printing the wrapped document without altering the 545 // current mode. 546 Document::ForceBroken(document) | Document::NextBreakFits(document, _) => { 547 docs.push_front((indent, mode, document)); 548 } 549 } 550 } 551 Ok(()) 552} 553 554pub fn nil<'a>() -> Document<'a> { 555 Document::Vec(vec![]) 556} 557 558pub fn line<'a>() -> Document<'a> { 559 Document::Line(1) 560} 561 562pub fn lines<'a>(i: usize) -> Document<'a> { 563 Document::Line(i) 564} 565 566pub fn break_<'a>(broken: &'a str, unbroken: &'a str) -> Document<'a> { 567 Document::Break { 568 broken, 569 unbroken, 570 kind: BreakKind::Strict, 571 } 572} 573 574pub fn flex_break<'a>(broken: &'a str, unbroken: &'a str) -> Document<'a> { 575 Document::Break { 576 broken, 577 unbroken, 578 kind: BreakKind::Flex, 579 } 580} 581 582impl<'a> Document<'a> { 583 pub fn group(self) -> Self { 584 Self::Group(Box::new(self)) 585 } 586 587 pub fn set_nesting(self, indent: isize) -> Self { 588 Self::Nest(indent, NestMode::Set, NestCondition::Always, Box::new(self)) 589 } 590 591 pub fn nest(self, indent: isize) -> Self { 592 Self::Nest( 593 indent, 594 NestMode::Increase, 595 NestCondition::Always, 596 Box::new(self), 597 ) 598 } 599 600 pub fn nest_if_broken(self, indent: isize) -> Self { 601 Self::Nest( 602 indent, 603 NestMode::Increase, 604 NestCondition::IfBroken, 605 Box::new(self), 606 ) 607 } 608 609 pub fn force_break(self) -> Self { 610 Self::ForceBroken(Box::new(self)) 611 } 612 613 pub fn next_break_fits(self, mode: NextBreakFitsMode) -> Self { 614 Self::NextBreakFits(Box::new(self), mode) 615 } 616 617 pub fn append(self, second: impl Documentable<'a>) -> Self { 618 match self { 619 Self::Vec(mut vec) => { 620 vec.push(second.to_doc()); 621 Self::Vec(vec) 622 } 623 first => Self::Vec(vec![first, second.to_doc()]), 624 } 625 } 626 627 pub fn to_pretty_string(self, limit: isize) -> String { 628 let mut buffer = String::new(); 629 self.pretty_print(limit, &mut buffer) 630 .expect("Writing to string buffer failed"); 631 buffer 632 } 633 634 pub fn surround(self, open: impl Documentable<'a>, closed: impl Documentable<'a>) -> Self { 635 open.to_doc().append(self).append(closed) 636 } 637 638 pub fn pretty_print(&self, limit: isize, writer: &mut impl Utf8Writer) -> Result<()> { 639 let docs = im::vector![(0, Mode::Unbroken, self)]; 640 format(writer, limit, 0, docs)?; 641 Ok(()) 642 } 643 644 /// Returns true when the document contains no printable characters 645 /// (whitespace and newlines are considered printable characters). 646 pub fn is_empty(&self) -> bool { 647 use Document::*; 648 match self { 649 Line(n) => *n == 0, 650 EcoString(s) => s.is_empty(), 651 String(s) => s.is_empty(), 652 Str(s) => s.is_empty(), 653 // assuming `broken` and `unbroken` are equivalent 654 Break { broken, .. } => broken.is_empty(), 655 ForceBroken(d) | Nest(_, _, _, d) | Group(d) | NextBreakFits(d, _) => d.is_empty(), 656 Vec(docs) => docs.iter().all(|d| d.is_empty()), 657 } 658 } 659}