pretty_expressive/lib.rs
1#![deny(missing_docs)]
2#![deny(rustdoc::broken_intra_doc_links)]
3#![deny(rustdoc::unescaped_backticks)]
4
5//! This crate is a Rust port of the [pretty-expressive][] printer from Racket.
6//! It is an efficient pretty printer with an expressive language for describing
7//! documents. The algorithm is described in ["A Pretty Expressive Printer"][paper].
8//! The two official implementations, in [Racket][racket-impl] and [OCaml][ocaml-impl],
9//! were both instrumental to being able to port the library to Rust.
10//!
11//! [pretty-expressive]: https://docs.racket-lang.org/pretty-expressive/
12//! [paper]: https://dl.acm.org/doi/abs/10.1145/3622837
13//! [racket-impl]: https://github.com/sorawee/pretty-expressive
14//! [ocaml-impl]: https://github.com/sorawee/pretty-expressive-ocaml
15//!
16//! # Overview
17//!
18//! To use the pretty printer, you need to construct an "abstract document" (a [`Doc`] in
19//! this library). This document contains not just the text you want to format, but also
20//! instructions about the different ways that it is valid to format it. `pretty-expressive`
21//! provides a variety of functions and operators to help you construct these documents.
22//!
23//! Let's look at a small example:
24//!
25//! ```
26//! # use pretty_expressive::*;
27//! let then_expr = text("'even");
28//! let else_expr = text("'odd");
29//! let cond_expr = text("(zero? (mod n 2))");
30//!
31//! let one_line = cond_expr.clone() &
32//! space() &
33//! then_expr.clone() &
34//! space() &
35//! else_expr.clone();
36//!
37//! let one_column = align(cond_expr.clone() &
38//! nl() &
39//! then_expr.clone() &
40//! nl() &
41//! else_expr.clone());
42//!
43//! let if_expr = lparen() &
44//! text("if ") &
45//! (one_line | one_column) &
46//! rparen();
47//!
48//! let doc = lparen() &
49//! text("defn even? (n)") &
50//! nest(2, nl() & if_expr & rparen());
51//!
52//! # assert_eq!(doc.to_string(),
53//! # r"(defn even? (n)
54//! # (if (zero? (mod n 2)) 'even 'odd))");
55//! ```
56//!
57//! It may look overwhelming at first, but if you can look past all the `clone()`s, it's
58//! expressing something pretty simple. This document contains the code to define a small
59//! Lisp function to check if a number is even. To create the document:
60//!
61//! 1. The three inputs to the `if` expression are created with [`text`].
62//! 2. We reuse those three documents to describe two different layouts for how those inputs
63//! could be formatted. One places all three on the same line, while the other aligns them
64//! vertically in a column. The [`&`](Doc::bitand) operator is used to concatenate the
65//! smaller documents together with punctuation into a larger document.
66//! 3. The `if` expression combines the two layouts in one document using the [`|`](Doc::bitor)
67//! operator. This gives the printer a choice. It has to choose one of these two layouts
68//! to render for this portion of the document. The printer will decide which is more
69//! optimal and use that one.
70//! 4. Finally, the final document is built by combining the if expression with other
71//! documents to describe the layout of the entire function definition. [`nest`] is used to
72//! indent the body of the `defn` (i.e. the `if` expression) by 2 spaces.
73//!
74//! Now that we've constructed the document, we can call `doc.to_string()` to render it to a
75//! [`String`].
76//!
77//! ```
78//! # use pretty_expressive::*;
79//! # let then_expr = text("'even");
80//! # let else_expr = text("'odd");
81//! # let cond_expr = text("(zero? (mod n 2))");
82//! #
83//! # let if_expr = lparen() & text("if ") &
84//! # ((cond_expr.clone() & space() & then_expr.clone() &
85//! # space() & else_expr.clone()) |
86//! # align(cond_expr.clone() & nl() & then_expr.clone() &
87//! # nl() & else_expr.clone())) & rparen();
88//! # let doc = lparen() & text("defn even? (n)") &
89//! # nest(2, nl() & if_expr & rparen());
90//! assert_eq!(doc.to_string(),
91//! r"(defn even? (n)
92//! (if (zero? (mod n 2)) 'even 'odd))");
93//! ```
94//!
95//! The main constraint we can impose on the printer is the _page width_: the maximum number
96//! of characters we want to fit on a single line. Calling `to_string()` uses a default page
97//! width of 80 characters. If we want to use a different page width, we can use the [`format!`]
98//! macro.
99//!
100//! Since there's plenty of room, the printer opted to put all three `if` arguments on one
101//! line together. If we use a smaller page width, the printer makes a different choice:
102//!
103//! ```
104//! # use pretty_expressive::*;
105//! # let then_expr = text("'even");
106//! # let else_expr = text("'odd");
107//! # let cond_expr = text("(zero? (mod n 2))");
108//! #
109//! # let if_expr = lparen() & text("if ") &
110//! # ((cond_expr.clone() & space() & then_expr.clone() &
111//! # space() & else_expr.clone()) |
112//! # align(cond_expr.clone() & nl() & then_expr.clone() &
113//! # nl() & else_expr.clone())) & rparen();
114//! # let doc = lparen() & text("defn even? (n)") &
115//! # nest(2, nl() & if_expr & rparen());
116//! assert_eq!(format!("{doc:20}"),
117//! r"(defn even? (n)
118//! (if (zero? (mod n 2))
119//! 'even
120//! 'odd))");
121//! ```
122//!
123//! It might seem contrived to use a width so small: nobody would actually try to format
124//! their code into a width of 20 characters! That's probably true, but imagine this
125//! expression appears nested much further into a function, and further right in the line
126//! it appears on. The document we created here describes the valid ways for this
127//! expression to be formatted _no matter where in the overall document it appears_.
128//!
129//! This means we can write a Rust function that can describe how any `if` expression should
130//! be formatted (this example isn't far off from doing so already). We can then do the same
131//! for every possible construct in our language that needs formatting, composing them
132//! together as needed. In the end, we have a program that can format any document in our
133//! language, built from simple descriptions of the possible layouts each piece could use.
134//!
135//! # A more complete example
136//!
137//! I ported this library to Rust because I wanted to build a code formatter for MJL, a Lisp
138//! language I'm creating. Other approaches I tried for solving the problem didn't get me the
139//! results I was hoping for, and the code was much more convoluted. The [version I built with
140//! `pretty-expressive`](https://git.midna.dev/mjm/mjl/src/branch/main/mjl-format/src/lib.rs)
141//! handles more cases correctly and is significantly easier to read and reason about.
142//!
143//! # Creating abstract documents
144//!
145//! The documentation for [`Doc`] describes the different functions and operators used to
146//! create, wrap, and combine abstract documents to prepare them to be printed.
147//!
148//! # Printing a document
149//!
150//! The example above used `to_string()` and [`format!`] to turn an abstract document into
151//! a [`String`]. If you have another destination you want to write to, you can instead use
152//! one of the other Rust formatting macros to send the printed document wherever you like.
153//!
154//! ```
155//! # use pretty_expressive::*;
156//!
157//! let doc = text("hello world");
158//! println!("{doc:120}");
159//! ```
160//!
161//! You can use the [width](std::fmt#width) parameter in the macro to request a particular
162//! page width.
163//!
164//! # Adjusting the printer's notion of "optimal"
165//!
166//! The pretty printer chooses an optimal layout by assigning a [`Cost`] to each layout it
167//! explores. By default, it's designed to optimize first for not exceeding the desired page
168//! width and second for minimizing the number of lines produced. In other words, it will
169//! try to fit as much as it can on each line, only using more lines when it has no other
170//! choice or it would make the document too wide.
171//!
172//! You can probably get pretty far using this default, but there are some ways to tweak it
173//! if needed:
174//!
175//! * Use the [`cost`] function to increase the cost imposed on a particular document, which
176//! can cause the printer to choose an alternative layout instead.
177//! * Replace the [`CostFactory`] that the printer uses to assign costs. If you do this, be
178//! sure to read the documentation carefully, as there are rules that costs and cost factories
179//! must follow for the printer to work correctly.
180//!
181//! # Credits
182//!
183//! My thanks to the authors of ["A Pretty Expressive Printer"][paper]:
184//!
185//! * Sorawee Porncharoenwase
186//! * Justin Pombrio
187//! * Emina Torlak
188//!
189//! This crate is almost entirely based on their ideas and using their implementations as a
190//! reference.
191
192mod cost;
193mod measure;
194mod non_empty;
195mod print;
196
197use std::{
198 cell::Cell,
199 collections::HashMap,
200 ops::{BitAnd, BitOr},
201 rc::Rc,
202};
203
204pub use cost::{Cost, CostFactory, DefaultCost, DefaultCostFactory};
205pub use print::{Error, PrintResult, Result};
206
207const MEMO_LIMIT: i8 = 7;
208thread_local! {
209 static GLOBAL_ID: Cell<usize> = const { Cell::new(0) };
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
213struct DocId(usize);
214
215/// An abstract document containing pretty printing instructions.
216///
217/// The `Doc` contains the tree of instructions and choices to present to the
218/// pretty printer. The printer uses this information to choose an optimal
219/// layout and produce it when asked.
220///
221/// A `Doc` is internally wrapped in an [`Rc`], so it is cheap to clone. It is
222/// advantageous to `clone` and reuse a `Doc` when creating different choices
223/// of layout for the same content, as this will save the printer from repeating
224/// work. See the docs for [`|`](Self#sharing-subdocuments) for more
225/// details on that.
226///
227/// # Constructing documents
228///
229/// Documents are constructed by starting with small pieces of text, which are
230/// then wrapped and composed with various operators to describe constraints on
231/// how they should be laid out by the printer.
232///
233/// ## Creating documents with content
234///
235/// The two basic atomic units of documents are:
236///
237/// * [`text`]: Renders some text verbatim
238/// * [`newline`]: Moves the printer to the next line, possibly with indentation
239///
240/// All of the actual content in a document comes from these two functions or
241/// convenience functions that wrap them.
242///
243/// ## Wrapping documents to modify behavior
244///
245/// There are also documents that wrap other documents to modify how they are
246/// rendered by the printer:
247///
248/// * [`nest`]: Increases the indentation level for the document
249/// * [`align`]: Aligns subsequent lines in the document to its starting column
250/// * [`reset`]: Resets the indentation level to 0
251/// * [`full`]: Requires the document to take up the remainder of its line
252/// * [`cost`]: Artificially increase the cost of a document
253///
254/// Using these modifiers can help get exactly the desired layout with minimal
255/// fuss.
256///
257/// ## Combining documents
258///
259/// Documents can be combined to create more complex ones using two operators:
260///
261/// * [`&`](Self::bitand): Concatenate documents to form a larger one
262/// * [`|`](Self::bitor): Create a choice between two documents
263///
264/// These two operators are what enables the printer to optimally format complex
265/// documents. Small documents can be combined into larger ones representing an
266/// entire piece of text, and creating choices along the way gives the printer
267/// options for different ways to format, allowing it to choose an optimal
268/// layout.
269///
270/// # Printing documents
271///
272/// `Doc` implements [`Display`](std::fmt::Display), so they can be printed
273/// in all the usual ways you expect to be able to print things in Rust. By
274/// default, the doc will be constrained to a page width of 80 characters. If
275/// you're using one of [Rust's formatting macros](std::fmt#related-macros),
276/// then you can use a width parameter to customize this.
277///
278/// ## What if it fails to render?
279///
280/// Rendering a document can [`fail`]. Generally when this happens, it means
281/// that you've put too tight of constraints on the choices given, so the
282/// printer isn't able to find an option that is allowed. If you use `Doc`'s
283/// implementation of [`Display`](std::fmt::Display) with a document that fails
284/// to render, it will panic, as `Display` doesn't give a way to surface custom
285/// errors. Usually, this is fine: correctly constructed documents for printing
286/// should always have a valid choice path, even if some of the subdocuments
287/// within do not.
288///
289/// If you want to be able to detect when a document has failed to print,
290/// you can call [`doc.validate(page_width)`](Self::validate). This will run
291/// the printer up to the point where it chooses a layout and return either
292/// the layout chosen or an [`Error`]. The returned layout also implements
293/// `Display`, so it can be printed from there. Note that since the page width
294/// was chosen when `validate` was called (since it was needed for the printer
295/// to choose the layout), the width cannot be customized in format macros when
296/// printing a layout.
297///
298/// # Thread safety
299///
300/// Due to their use of [`Rc`] internally, `Doc`s cannot be moved from the thread
301/// where they are created. They also use a thread-local static counter to assign
302/// a unique ID to each `Doc`, so moving them between threads would also introduce
303/// the possibility of collisions for these IDs.
304#[derive(Debug)]
305pub struct Doc<C: Cost = DefaultCost>(Rc<DocInner<C>>);
306
307#[derive(Debug)]
308struct DocInner<C: Cost> {
309 kind: DocKind<C>,
310 id: DocId,
311 memo_weight: i8,
312 newline_count: usize,
313}
314
315#[derive(Debug)]
316enum DocKind<C: Cost> {
317 Newline(Option<String>),
318 Fail,
319 Text(String, usize),
320 Concat(Doc<C>, Doc<C>),
321 Alt(Doc<C>, Doc<C>),
322 Align(Doc<C>),
323 Reset(Doc<C>),
324 Nest(usize, Doc<C>),
325 Full(Doc<C>),
326 Cost(C, Doc<C>),
327}
328
329macro_rules! def_newline {
330 ($name:ident, $val:literal) => {
331 #[doc = concat!("Creates a newline document that flattens to `", stringify!($val), "`.")]
332 pub fn $name<C: Cost>() -> Doc<C> {
333 newline(Some($val))
334 }
335 };
336}
337
338macro_rules! def_text {
339 ($name:ident, $val:literal) => {
340 #[doc = concat!("Creates a document that renders the text `", stringify!($val), "`.")]
341 pub fn $name<C: Cost>() -> Doc<C> {
342 text($val)
343 }
344 };
345}
346
347def_newline!(nl, " ");
348def_newline!(brk, "");
349
350def_text!(comma, ",");
351def_text!(lbrack, "[");
352def_text!(rbrack, "]");
353def_text!(lbrace, "{");
354def_text!(rbrace, "}");
355def_text!(lparen, "(");
356def_text!(rparen, ")");
357def_text!(dquote, "\"");
358def_text!(space, " ");
359
360/// A newline that cannot be flattened.
361///
362/// Equivalent to `newline(None)`.
363///
364/// # Example
365///
366/// ```
367/// # use pretty_expressive::*;
368/// let doc = hard_nl();
369/// assert_eq!(doc.to_string(), "\n");
370///
371/// let doc = flatten(hard_nl());
372/// assert!(matches!(doc.validate(80), Err(pretty_expressive::Error)));
373///
374/// let doc = flatten(hard_nl() | text("something else"));
375/// assert_eq!(doc.to_string(), "something else");
376/// ```
377pub fn hard_nl<C: Cost>() -> Doc<C> {
378 newline::<C, String>(None)
379}
380
381/// Creates a newline document.
382///
383/// The document renders to a newline character (`'\n'`) followed by the current
384/// level of indentation spaces.
385///
386/// When [`flatten`]ed, if `s` is `Some`, it renders to the string it contains.
387/// If `s` is `None`, then when flattened the document will fail to render.
388///
389/// # Example
390///
391/// ```
392/// # use pretty_expressive::*;
393/// let doc = text("[") &
394/// newline(Some("")) &
395/// text("a") &
396/// newline(Some(", ")) &
397/// text("b") &
398/// newline(Some("")) &
399/// text("]");
400/// assert_eq!(doc.to_string(),
401/// r"[
402/// a
403/// b
404/// ]");
405///
406/// let doc = flatten(doc);
407/// assert_eq!(doc.to_string(), "[a, b]");
408/// ```
409///
410/// # Convenience aliases
411///
412/// This crate defines some functions for common uses of newlines:
413///
414/// * [`nl`]: Flattens to `" "` (a single space).
415/// * [`brk`]: Flattens to `""` (empty text).
416/// * [`hard_nl`]: Fails to flatten.
417pub fn newline<C: Cost, S: ToString>(s: Option<S>) -> Doc<C> {
418 Doc(Rc::new(DocInner {
419 kind: DocKind::Newline(s.map(|s| s.to_string())),
420 id: new_id(),
421 memo_weight: MEMO_LIMIT - 1,
422 newline_count: 1,
423 }))
424}
425
426/// Creates a document that fails to render.
427///
428/// This is most useful when used with [choices](Doc::bitor) by making one path
429/// fail to render, forcing the printer to choose an alternative.
430///
431/// # Example
432///
433/// ```
434/// # use pretty_expressive::*;
435/// let doc = fail();
436/// assert!(matches!(doc.validate(80), Err(pretty_expressive::Error)));
437///
438/// let doc = fail() | text("not a fail");
439/// assert_eq!(doc.to_string(), "not a fail");
440/// ```
441pub fn fail<C: Cost>() -> Doc<C> {
442 Doc(Rc::new(DocInner {
443 kind: DocKind::Fail,
444 id: new_id(),
445 memo_weight: MEMO_LIMIT - 1,
446 newline_count: 0,
447 }))
448}
449
450/// Creates a document that renders a single-line string.
451///
452/// It is very important that the string not contain any newline characters
453/// (`'\n'`), as the printer expects that newlines only come from [`newline`]
454/// documents. A `text` with newlines in it will cause the printer to have an
455/// incorrect perception of the layouts it is choosing between.
456///
457/// # Example
458///
459/// ```
460/// # use pretty_expressive::*;
461/// let doc = text("Bulbasaur");
462/// assert_eq!(doc.to_string(), "Bulbasaur");
463/// ```
464///
465/// # Convenience aliases
466///
467/// This crate defines some functions for common bits of text:
468///
469/// * [`comma`][]: `","`
470/// * [`lbrack`][]: `"["`
471/// * [`rbrack`][]: `"]"`
472/// * [`lbrace`][]: `"{"`
473/// * [`rbrace`][]: `"}"`
474/// * [`lparen`][]: `"("`
475/// * [`rparen`][]: `")"`
476/// * [`dquote`][]: `"\""`
477/// * [`space`][]: `" "`
478pub fn text<C: Cost>(s: impl ToString) -> Doc<C> {
479 let s = s.to_string();
480 let len = s.chars().count();
481
482 Doc(Rc::new(DocInner {
483 kind: DocKind::Text(s, len),
484 id: new_id(),
485 memo_weight: MEMO_LIMIT - 1,
486 newline_count: 0,
487 }))
488}
489
490/// Increase the indentation level while rendering a document.
491///
492/// Indentation is only affected after the next [`newline`] that is rendered.
493/// Content on the current line will not be indented.
494///
495/// # Example
496///
497/// ```
498/// # use pretty_expressive::*;
499/// let doc = text("Bulbasaur") &
500/// nest(4, text("Charmander") & nl() & text("Squirtle"));
501/// assert_eq!(r"BulbasaurCharmander
502/// Squirtle", doc.to_string());
503/// ```
504pub fn nest<C: Cost>(n: usize, d: Doc<C>) -> Doc<C> {
505 use DocKind::*;
506
507 match &d.0.kind {
508 Fail | Align(_) | Reset(_) | Text(_, _) => d,
509 Cost(c, d) => cost(c.clone(), nest(n, d.clone())),
510 _ => {
511 let memo_weight = d.weight();
512 let newline_count = d.0.newline_count;
513
514 Doc(Rc::new(DocInner {
515 kind: DocKind::Nest(n, d),
516 id: new_id(),
517 memo_weight,
518 newline_count,
519 }))
520 }
521 }
522}
523
524/// Aligns a document.
525///
526/// Aligning resets the indentation level while rendering `d` to the starting
527/// column of `d`. Any [`newline`]s within `d` will line up with that column.
528/// Any uses of [`nest`] within `d` will increase the indentation starting from
529/// that column.
530///
531/// After rendering `d`, the indentation level will return to its previous
532/// value.
533///
534/// Alignment is particularly useful for formatting Lisp code, as Lisps tend to
535/// prefer to align children based on the start of their parent, rather than
536/// relative to an overall indentation level.
537///
538/// # Example
539///
540/// ```
541/// # use pretty_expressive::*;
542/// let doc = nl() &
543/// lparen() &
544/// text("+") &
545/// space() &
546/// align(text("123") &
547/// nl() &
548/// align(text("(let ((a 454))") &
549/// nest(2, nl() &
550/// text("(+ a 2)))"))));
551///
552/// assert_eq!(doc.to_string(), r"
553/// (+ 123
554/// (let ((a 454))
555/// (+ a 2)))");
556/// ```
557///
558/// This example uses two alignment points. The first wraps the arguments to
559/// `+` so that they line up in a column. The second sets the alignment for
560/// the `let` form, so that the `nest` inside it will indent subsequent lines
561/// two spaces from the start of the `let`. That's not strictly needed in this
562/// specific case, since the outer `align` was at the same column, but the inner
563/// `align` ensures that wherever the `let` may appear in the document, it will
564/// be indented correctly.
565pub fn align<C: Cost>(d: Doc<C>) -> Doc<C> {
566 use DocKind::*;
567
568 match &d.0.kind {
569 Fail | Align(_) | Reset(_) | Text(_, _) => d,
570 Cost(c, d) => cost(c.clone(), align(d.clone())),
571 _ => {
572 let memo_weight = d.weight();
573 let newline_count = d.0.newline_count;
574
575 Doc(Rc::new(DocInner {
576 kind: DocKind::Align(d),
577 id: new_id(),
578 memo_weight,
579 newline_count,
580 }))
581 }
582 }
583}
584
585/// Resets the indentation level for a document to 0.
586///
587/// This can be useful for things like multiline strings, where the indentation
588/// is temporarily reset.
589///
590/// After rendering `d`, the indentation level will return to its previous
591/// value.
592///
593/// # Example
594///
595/// ```
596/// # use pretty_expressive::*;
597/// let doc = text("def gen1_starters():") &
598/// nest(4, nl() &
599/// text("print ") &
600/// reset(text("'''") & nl() &
601/// text("Bulbasaur") & nl() &
602/// text("Charmander") & nl() &
603/// text("Squirtle") & nl() &
604/// text("'''")) &
605/// nl() &
606/// text("return True"));
607///
608/// assert_eq!(doc.to_string(),
609/// r"def gen1_starters():
610/// print '''
611/// Bulbasaur
612/// Charmander
613/// Squirtle
614/// '''
615/// return True");
616/// ```
617pub fn reset<C: Cost>(d: Doc<C>) -> Doc<C> {
618 use DocKind::*;
619
620 match &d.0.kind {
621 Fail | Align(_) | Reset(_) | Text(_, _) => d,
622 Cost(c, d) => cost(c.clone(), reset(d.clone())),
623 _ => {
624 let memo_weight = d.weight();
625 let newline_count = d.0.newline_count;
626
627 Doc(Rc::new(DocInner {
628 kind: DocKind::Reset(d),
629 id: new_id(),
630 memo_weight,
631 newline_count,
632 }))
633 }
634 }
635}
636
637/// Requires that a document not be followed by any more text on the same line.
638///
639/// If more text is placed after this document without moving to a new line,
640/// the document will fail to render. Note that this doesn't introduce the new
641/// line on its own. Instead, it forces the printer to choose paths that will
642/// uphold the constraint.
643///
644/// A typical use-case for this is line comments, where additional code cannot
645/// be placed after the comment without becoming part of the comment text. By
646/// wrapping the comment text in `full`, you ensure that the printer will not
647/// produce a layout that changes the meaning of the document by placing code
648/// tokens on the comment line.
649///
650/// # Example
651///
652/// ```
653/// # use pretty_expressive::*;
654/// let comment = full(text(";; this comment can't have code after it"));
655/// let code = text("(print 123)");
656///
657/// let doc = comment.clone() & nl() & code.clone();
658/// assert_eq!(doc.to_string(),
659/// r";; this comment can't have code after it
660/// (print 123)");
661///
662/// let doc = comment.clone() & code.clone();
663/// assert!(matches!(doc.validate(80), Err(pretty_expressive::Error)));
664///
665/// // text afterwards is allowed if it is zero-length
666/// let doc = comment.clone() & text("");
667/// assert_eq!(
668/// doc.to_string(),
669/// ";; this comment can't have code after it"
670/// );
671/// ```
672pub fn full<C: Cost>(d: Doc<C>) -> Doc<C> {
673 use DocKind::*;
674
675 match &d.0.kind {
676 Full(_) => d,
677 Fail => fail(),
678 _ => {
679 let memo_weight = d.weight();
680 let newline_count = d.0.newline_count;
681
682 Doc(Rc::new(DocInner {
683 kind: DocKind::Full(d),
684 id: new_id(),
685 memo_weight,
686 newline_count,
687 }))
688 }
689 }
690}
691
692/// Introduces an extra cost to a document.
693///
694/// This can be used to steer the printer away from making certain choices while
695/// still including them as an option. The cost `c` will be added to whatever
696/// cost would normally be assigned to the document.
697///
698/// # Example
699///
700/// Normally, when everything fits in the page width, the printer will prefer
701/// to print things flatter, since it adds a cost for each newline added to
702/// the layout.
703///
704/// ```
705/// # use pretty_expressive::*;
706/// let doc = text("hello world") | (text("hello") & hard_nl() & text("world"));
707/// assert_eq!(doc.to_string(), "hello world");
708/// ```
709///
710/// We can make the flatter option have a greater cost, which will cause the
711/// printer to choose the taller layout instead.
712///
713/// ```
714/// # use pretty_expressive::*;
715/// let doc = cost(DefaultCost(0, 2), text("hello world")) |
716/// (text("hello") & hard_nl() & text("world"));
717///
718/// assert_eq!(doc.to_string(),
719/// r"hello
720/// world");
721/// ```
722pub fn cost<C: Cost>(c: C, d: Doc<C>) -> Doc<C> {
723 if matches!(&d.0.kind, DocKind::Fail) {
724 d
725 } else {
726 let memo_weight = d.weight();
727 let newline_count = d.0.newline_count;
728
729 Doc(Rc::new(DocInner {
730 kind: DocKind::Cost(c, d),
731 id: new_id(),
732 memo_weight,
733 newline_count,
734 }))
735 }
736}
737
738/// The `&` operator is used to concatenate documents.
739///
740/// This is the fundamental operator for creating a larger document out of
741/// smaller ones.
742impl<C: Cost> BitAnd<Doc<C>> for Doc<C> {
743 /// Concatenation forms a new document.
744 type Output = Doc<C>;
745
746 /// Concatenates two documents.
747 ///
748 /// This places the documents one after the other in the layout. No
749 /// additional spacing or alignment is added.
750 ///
751 /// # Example
752 ///
753 /// ```
754 /// # use pretty_expressive::*;
755 /// let doc = text("hello") & space() & text("world");
756 /// assert_eq!(doc.to_string(), "hello world");
757 /// ```
758 fn bitand(self, rhs: Doc<C>) -> Self::Output {
759 use DocKind::*;
760
761 match (&self.0.kind, &rhs.0.kind) {
762 (Fail, _) | (_, Fail) => fail(),
763 (Text(_, 0), _) => rhs,
764 (_, Text(_, 0)) => self,
765 (Full(_), Text(_, _)) => fail(),
766 (Text(s1, l1), Text(s2, l2)) => {
767 let mut s = s1.clone();
768 s.push_str(s2.as_str());
769 Doc(Rc::new(DocInner {
770 kind: DocKind::Text(s, l1 + l2),
771 id: new_id(),
772 memo_weight: MEMO_LIMIT - 1,
773 newline_count: 0,
774 }))
775 }
776 (_, Cost(c, d2)) => cost(c.clone(), self & d2.clone()),
777 (Cost(c, d1), _) => cost(c.clone(), d1.clone() & rhs),
778 _ => {
779 let left_nl_count = self.0.newline_count;
780 let right_nl_count = rhs.0.newline_count;
781 let memo_weight = self.weight().min(rhs.weight());
782
783 Doc(Rc::new(DocInner {
784 kind: DocKind::Concat(self, rhs),
785 id: new_id(),
786 memo_weight,
787 newline_count: left_nl_count + right_nl_count,
788 }))
789 }
790 }
791 }
792}
793
794/// The `|` operator is used to create a choice between multiple documents.
795///
796/// This is the fundamental operator for giving the printer options for
797/// different ways to layout a document.
798impl<C: Cost> BitOr<Doc<C>> for Doc<C> {
799 /// A choice between two documents is a new document.
800 type Output = Doc<C>;
801
802 /// Creates a document that will be rendered to one of two documents.
803 ///
804 /// The printer will determine which side of the choice is prettier and will
805 /// render using that layout.
806 ///
807 /// A choice should generally include the same content, but with different
808 /// formatting. That said, there is no restriction on what appears on either
809 /// side of a choice: they can be completely different if that is what
810 /// serves your needs.
811 ///
812 /// # Example
813 ///
814 /// ```
815 /// # use pretty_expressive::*;
816 /// let doc = text("hello world") |
817 /// (text("hello") & hard_nl() & text("world"));
818 /// assert_eq!(doc.to_string(), "hello world");
819 /// ```
820 ///
821 /// # Sharing subdocuments
822 ///
823 /// The printer is able to take advantage of the fact that the tree of
824 /// choices for a document isn't actually a tree: it's a DAG (directed
825 /// acyclic graph). Because much of the content on either side of the
826 /// choice is the same, the printer can reuse calculations if it knows which
827 /// subdocuments are equivalent. Finding the optimal layout for the same
828 /// document at the same column and indentation level will always give the
829 /// same result, but the printer might reach that through many different
830 /// paths through the choice graph.
831 ///
832 /// To cache computed results, each [`Doc`] is assigned an ID internally.
833 /// `Doc`s are reference-counted (using [`Rc`]) internally, so when you
834 /// `clone()` a `Doc`, you get another reference to the same document, with
835 /// the same ID. For this reason, cloning documents to reuse on different
836 /// sides of a choice is ideal. So this:
837 ///
838 /// ```
839 /// # use pretty_expressive::*;
840 /// let hello = text("hello");
841 /// let world = text("world");
842 ///
843 /// let doc = v_append(hello.clone(), world.clone()) |
844 /// us_append(hello.clone(), world.clone());
845 /// # assert_eq!(doc.to_string(), "hello world");
846 /// ```
847 ///
848 /// is always preferable to this:
849 ///
850 /// ```
851 /// # use pretty_expressive::*;
852 /// let doc = v_append(text("hello"), text("world")) |
853 /// us_append(text("hello"), text("world"));
854 /// # assert_eq!(doc.to_string(), "hello world");
855 /// ```
856 ///
857 /// Even if they will produce the same result. It may not make a big
858 /// difference in this small of an example, but in practice, there can be
859 /// many subdocuments behind each side of a choice, and each subsequent
860 /// choice compounds that exponentially.
861 fn bitor(self, rhs: Doc<C>) -> Self::Output {
862 use DocKind::Fail;
863
864 match (&self.0.kind, &rhs.0.kind) {
865 (Fail, _) => rhs,
866 (_, Fail) => self,
867 _ => {
868 let left_nl_count = self.0.newline_count;
869 let right_nl_count = rhs.0.newline_count;
870 let memo_weight = self.weight().min(rhs.weight());
871
872 Doc(Rc::new(DocInner {
873 kind: DocKind::Alt(self, rhs),
874 id: new_id(),
875 memo_weight,
876 newline_count: left_nl_count.max(right_nl_count),
877 }))
878 }
879 }
880 }
881}
882
883impl<C: Cost> Clone for Doc<C> {
884 /// Creates a new reference to the same document.
885 ///
886 /// `Doc`s are internally reference-counted.
887 fn clone(&self) -> Self {
888 Doc(Rc::clone(&self.0))
889 }
890}
891
892macro_rules! def_append_concat {
893 (
894 $( #[$append_meta:meta] )*
895 fn $append_name:ident ( $x:ident , $y:ident ) $block:block
896
897 $( #[$concat_meta:meta] )*
898 fn $concat_name:ident;
899 ) => {
900 $( #[$append_meta] )*
901 pub fn $append_name<C: Cost>($x: Doc<C>, $y: Doc<C>) -> Doc<C> $block
902
903 $( #[$concat_meta] )*
904 pub fn $concat_name<C: Cost>(xs: impl IntoIterator<Item = Doc<C>>) -> Doc<C> {
905 let mut iter = xs.into_iter();
906 if let Some(next) = iter.next() {
907 iter.fold(next, $append_name)
908 } else {
909 text("")
910 }
911 }
912 }
913}
914
915def_append_concat! {
916 /// Concatenates two documents horizontally.
917 ///
918 /// To combine an iterator of documents, use [`concat()`].
919 ///
920 /// Equivalent to `x & y`. See [`Doc::bitand`] for more details.
921 fn u_append(x, y) { x & y }
922
923 /// Concatenates several documents into one.
924 ///
925 /// Equivalent to calling [`&`](Doc::bitand)/[`u_append`] successively to
926 /// combine the documents in the iterator. If the iterator is empty, it
927 /// returns `text("")`.
928 ///
929 /// # Example
930 ///
931 /// ```
932 /// # use pretty_expressive::*;
933 /// let doc = concat([text("hello"), space(), text("world")]);
934 /// assert_eq!(doc.to_string(), "hello world");
935 ///
936 /// let doc = concat([]);
937 /// assert_eq!(doc.to_string(), "");
938 /// ```
939 fn concat;
940}
941
942def_append_concat! {
943 /// Concatenates two documents vertically.
944 ///
945 /// The documents will be separated by an unflattenable newline. To combine
946 /// more than two documents or an iterator of documents, use [`v_concat`].
947 ///
948 /// Equivalent to `x & hard_nl() & y`.
949 ///
950 /// # Example
951 ///
952 /// ```
953 /// # use pretty_expressive::*;
954 /// let doc = v_append(text("hello"), text("world"));
955 /// assert_eq!(doc.to_string(), "hello\nworld");
956 /// ```
957 fn v_append(x, y) { x & hard_nl() & y }
958
959 /// Concatenates several documents vertically.
960 ///
961 /// Equivalent to calling [`v_append`] successively to combine the documents
962 /// in the iterator. If the iterator is empty, it returns `text("")`.
963 ///
964 /// # Example
965 ///
966 /// ```
967 /// # use pretty_expressive::*;
968 /// let doc = v_concat([text("hello"), text("my"), text("friend")]);
969 /// assert_eq!(doc.to_string(),
970 /// r"hello
971 /// my
972 /// friend");
973 ///
974 /// let doc = v_concat([]);
975 /// assert_eq!(doc.to_string(), "");
976 /// ```
977 fn v_concat;
978}
979
980def_append_concat! {
981 /// Concatenates two documents horizontally with alignment.
982 ///
983 /// If `y` contains newlines, those lines will stay aligned with
984 /// the start of `y`, rather than flowing back to the current indentation
985 /// level.
986 ///
987 /// To combine more than two documents or an iterator of documents, use
988 /// [`a_concat`].
989 ///
990 /// Equivalent to `x & align(y)`.
991 ///
992 /// # Example
993 ///
994 /// ```
995 /// # use pretty_expressive::*;
996 /// let doc = a_append(nl() & text("hello "),
997 /// text("my") & nl() & text("friend"));
998 /// assert_eq!(doc.to_string(), r"
999 /// hello my
1000 /// friend");
1001 /// ```
1002 fn a_append(x, y) { x & align(y) }
1003
1004 /// Concatenates several documents horizontally with alignment.
1005 ///
1006 /// Equivalent to calling [`a_append`] successively to combine the documents
1007 /// in the iterator. If the iterator is empty, it returns `text("")`.
1008 ///
1009 /// # Example
1010 ///
1011 /// ```
1012 /// # use pretty_expressive::*;
1013 /// let doc = a_concat([
1014 /// nl() & text("hello "),
1015 /// text("my") & nl() & text("friend "),
1016 /// text("how are") & nl() & text("you?"),
1017 /// ]);
1018 /// assert_eq!(doc.to_string(), r"
1019 /// hello my
1020 /// friend how are
1021 /// you?");
1022 ///
1023 /// let doc = a_concat([]);
1024 /// assert_eq!(doc.to_string(), "");
1025 /// ```
1026 fn a_concat;
1027}
1028
1029def_append_concat! {
1030 /// Concatenates two documents horizontally with spacing and alignment.
1031 ///
1032 /// Similar to [`a_append`], but with a space included between the documents.
1033 ///
1034 /// To combine more than two documents or an iterator of documents, use
1035 /// [`as_concat`].
1036 ///
1037 /// Equivalent to `x & space() & align(y)`.
1038 ///
1039 /// # Example
1040 ///
1041 /// ```
1042 /// # use pretty_expressive::*;
1043 /// let doc = as_append(nl() & text("hello"),
1044 /// text("my") & nl() & text("friend"));
1045 /// assert_eq!(doc.to_string(), r"
1046 /// hello my
1047 /// friend");
1048 /// ```
1049 fn as_append(x, y) { x & space() & align(y) }
1050
1051 /// Concatenates several documents horizontally with spacing and alignment.
1052 ///
1053 /// Equivalent to calling [`as_append`] successively to combine the documents
1054 /// in the iterator. If the iterator is empty, it returns `text("")`.
1055 ///
1056 /// # Example
1057 ///
1058 /// ```
1059 /// # use pretty_expressive::*;
1060 /// let doc = as_concat([
1061 /// nl() & text("hello"),
1062 /// text("my") & nl() & text("friend"),
1063 /// text("how are") & nl() & text("you?"),
1064 /// ]);
1065 /// assert_eq!(doc.to_string(), r"
1066 /// hello my
1067 /// friend how are
1068 /// you?");
1069 ///
1070 /// let doc = a_concat([]);
1071 /// assert_eq!(doc.to_string(), "");
1072 /// ```
1073 fn as_concat;
1074}
1075
1076def_append_concat! {
1077 /// Concatenates two documents horizontally with spacing.
1078 ///
1079 /// To combine more than two documents or an iterator of documents, use
1080 /// [`us_concat`].
1081 ///
1082 /// Equivalent to `x & space() & y`.
1083 ///
1084 /// # Example
1085 ///
1086 /// ```
1087 /// # use pretty_expressive::*;
1088 /// let doc = us_append(text("hello"), text("world"));
1089 /// assert_eq!(doc.to_string(), "hello world");
1090 /// ```
1091 fn us_append(x, y) { x & space() & y }
1092
1093 /// Concatenates several documents horizontally with spacing.
1094 ///
1095 /// Equivalent to calling [`us_append`] successively to combine the documents
1096 /// in the iterator. If the iterator is empty, it returns `text("")`.
1097 ///
1098 /// # Example
1099 ///
1100 /// ```
1101 /// # use pretty_expressive::*;
1102 /// let doc = us_concat([text("hello"), text("world")]);
1103 /// assert_eq!(doc.to_string(), "hello world");
1104 ///
1105 /// let doc = us_concat([]);
1106 /// assert_eq!(doc.to_string(), "");
1107 /// ```
1108 fn us_concat;
1109}
1110
1111/// Create a flattened version of a document.
1112///
1113/// Flattening converts all newlines within a document to the flat alternative
1114/// that was provided to [`newline`], producing a document that can be rendered
1115/// on a single line. If any newlines within the document provided `None` for
1116/// their alternative (e.g. [`hard_nl`]), then the flattened document will fail
1117/// to render, forcing the printer to choose another path.
1118///
1119/// # Example
1120///
1121/// ```
1122/// # use pretty_expressive::*;
1123/// let doc = text("a") & nl() & text("b") & nl() & text("c");
1124/// assert_eq!(doc.to_string(), r"a
1125/// b
1126/// c");
1127///
1128/// let doc = flatten(doc);
1129/// assert_eq!(doc.to_string(), "a b c");
1130///
1131/// let doc = text("a") & brk() & text("b") & brk() & text("c");
1132/// assert_eq!(doc.to_string(), r"a
1133/// b
1134/// c");
1135///
1136/// let doc = flatten(doc);
1137/// assert_eq!(doc.to_string(), "abc");
1138///
1139/// let doc = text("a") & hard_nl() & text("b") & hard_nl() & text("c");
1140/// assert_eq!(doc.to_string(), r"a
1141/// b
1142/// c");
1143///
1144/// let doc = flatten(doc);
1145/// assert!(matches!(doc.validate(80), Err(pretty_expressive::Error)));
1146/// ```
1147pub fn flatten<C: Cost>(x: Doc<C>) -> Doc<C> {
1148 use DocKind::*;
1149
1150 let mut cache: HashMap<DocId, Doc<C>> = HashMap::new();
1151
1152 fn flatten_inner<C: crate::cost::Cost>(
1153 cache: &mut HashMap<DocId, Doc<C>>,
1154 x: Doc<C>,
1155 ) -> Doc<C> {
1156 if let Some(cached) = cache.get(&x.0.id) {
1157 cached.clone()
1158 } else {
1159 let id = x.0.id;
1160 let result = {
1161 match &x.0.kind {
1162 Fail | Text(_, _) => x,
1163 Newline(None) => fail(),
1164 Newline(Some(s)) => text(s),
1165 Concat(a, b) => {
1166 let ap = flatten_inner(cache, a.clone());
1167 let bp = flatten_inner(cache, b.clone());
1168 if ap.0.id == a.0.id && bp.0.id == b.0.id {
1169 x
1170 } else {
1171 ap & bp
1172 }
1173 }
1174 Alt(a, b) => {
1175 let ap = flatten_inner(cache, a.clone());
1176 let bp = flatten_inner(cache, b.clone());
1177 if ap.0.id == a.0.id && bp.0.id == b.0.id {
1178 x
1179 } else {
1180 ap | bp
1181 }
1182 }
1183 Nest(_, d) | Align(d) | Reset(d) | Full(d) => flatten_inner(cache, d.clone()),
1184 Cost(c, d) => {
1185 let dp = flatten(d.clone());
1186 if dp.0.id == d.0.id {
1187 x
1188 } else {
1189 cost(c.clone(), dp)
1190 }
1191 }
1192 }
1193 };
1194 cache.insert(id, result.clone());
1195 result
1196 }
1197 }
1198
1199 flatten_inner(&mut cache, x)
1200}
1201
1202/// Create a choice between a document and its [`flatten`]ed version.
1203///
1204/// # Example
1205///
1206/// ```
1207/// # use pretty_expressive::*;
1208/// let doc = text("hello") & nl() & text("world");
1209/// assert_eq!(format!("{doc}"), "hello\nworld");
1210///
1211/// let doc = group(doc);
1212/// assert_eq!(format!("{doc}"), "hello world");
1213/// assert_eq!(format!("{doc:10}"), "hello\nworld");
1214/// ```
1215pub fn group<C: Cost>(x: Doc<C>) -> Doc<C> {
1216 x.clone() | flatten(x)
1217}
1218
1219impl<C: Cost> DocKind<C> {
1220 pub(crate) fn fails(&self, full_before: bool, full_after: bool) -> bool {
1221 use DocKind::*;
1222
1223 match (self, full_before, full_after) {
1224 (Fail, _, _) => true,
1225 (Newline(_), _, full_after) => full_after,
1226 (Text(_, _), false, false) => false,
1227 (Text(_, _), true, false) | (Text(_, _), false, true) => true,
1228 (Text(_, l), true, true) => *l > 0,
1229 (Full(_), _, full_after) => !full_after,
1230 _ => false,
1231 }
1232 }
1233}
1234
1235fn new_id() -> DocId {
1236 GLOBAL_ID.with(|id| {
1237 let new_id = id.get();
1238 id.set(new_id + 1);
1239 DocId(new_id)
1240 })
1241}
1242
1243impl<C: Cost> Doc<C> {
1244 fn weight(&self) -> i8 {
1245 if self.0.memo_weight == 0 {
1246 MEMO_LIMIT - 1
1247 } else {
1248 self.0.memo_weight - 1
1249 }
1250 }
1251}