A Pretty Expressive Printer
1. A Pretty Expressive Printer (with Appendices)
SORAWEE PORNCHAROENWASE, University of Washington, USA
JUSTIN POMBrio, Unaffiliated, USA
EMINA TORLAK, University of Washington, USA
Pretty printers make trade-offs between the expressiveness of their pretty printing language, the optimality objective that they minimize when choosing between different ways to lay out a document, and the performance of their algorithm. This paper presents a new pretty printer, , that is strictly more expressive than all pretty printers in the literature and provably minimizes an optimality objective. Furthermore, the time complexity of is better than many existing pretty printers. When choosing among different ways to lay out a document, consults a user-supplied cost factory, which determines the optimality objective, giving a unique degree of flexibility. We use the Lean theorem prover to verify the correctness (validity and optimality) of , and implement concretely as a pretty printer that we call PRETTYEXPRESSIVE. To evaluate our pretty printer against others, we develop a formal framework for reasoning about the expressiveness of pretty printing languages, and survey pretty printers in the literature, comparing their expressiveness, optimality, worst-case time complexity, and practical running time. Our evaluation shows that PRETTYEXPRESSIVE is efficient and effective at producing optimal layouts. PRETTYEXPRESSIVE has also seen real-world adoption: it serves as a foundation of a code formatter for Racket.
CCS Concepts: • Software and its engineering → Functional languages; • Mathematics of computing → Combinatorial optimization.
Additional Key Words and Phrases: pretty printing
1.1. ACM Reference Format:
Sorawee Porncharoenwase, Justin Pombrio, and Emina Torlak. 2023. A Pretty Expressive Printer (with Appendices). Proc. ACM Program. Lang. 7, OOPSLA2, Article 261 (October 2023), 34 pages. https://doi.org/10.1145/3622837
1.2. 1 INTRODUCTION
General-purpose pretty printers (or, simply, printers) are widely used to convert structured data—typically an AST—into human-readable text. Their applications include code reformatting, software reengineering, and synthesized code printing [De Jonge 2002; Prettier 2016; Torlak and Bodik 2014; Yelland 2015]. These printers take as inputs (1) a document in a pretty printing language (PPL), which encodes the structured data along with formatting choices, and (2) a page width limit. Choices in the document can yield exponentially many possible layouts. The task of the printers is then to efficiently choose an optimal layout from all possible layouts. Existing printers use a variety of built-in optimality objectives. A good objective reflects the informal notion of “prettiness,” such as not overflowing past the page width limit whenever possible, while having as few lines as possible.
Authors’ addresses: Sorawee Porncharoenwase, Paul G. Allen School of Computer Science & Engineering, University of Washington, Seattle, WA, USA, sorawee@cs.washington.edu; Justin Pombrio, Unaffiliated, Cambridge, MA, USA, jpombrio@cs.brown.edu; Emina Torlak, Paul G. Allen School of Computer Science & Engineering, University of Washington, Seattle, WA, USA, emina@cs.washington.edu.
Permission to make digital or hard copies of part or all of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. Copyrights for third-party components of this work must be honored. For all other uses, contact the owner/author(s).
© 2023 Copyright held by the owner/author(s).
2475-1421/2023/10-ART261
Different printers make different trade-offs in the expressiveness of the PPL, the optimality objective, and the performance. This paper presents a printer that we call . It targets , a PPL that is strictly more expressive than all published PPLs. This can be shown via our formal framework for reasoning about the expressiveness of PPLs. is parameterized by a cost factory, which enables users to specify an optimality objective for to minimize. The cost factory is versatile. For example, it can express non-linear costs and define concepts such as soft page width limits [Yelland 2016]. As a result, the optimal layout that chooses can have higher quality compared to existing printers. The time complexity of is , where is the size of the document and is the computation width limit (defined in Section 6). This is better than the time complexity of many printers in the literature, and it is improved to when is restricted to process documents in some well-known but less expressive PPLs. We prove the correctness of in the Lean theorem prover [Moura and Ullrich 2021], ensuring the validity and optimality of the output layout, and demonstrate 's efficiency by evaluating our implementation of , which we call PRETTYEXPRESSIVE. We believe these attributes make not only a good printer by itself, but also a good building block to construct other derived printers.
A Survey of Printers in the Wild. To evaluate , we conducted a broad survey of the literature on pretty printing. Most PPLs, embedded in a host programming language, provide a small set of core constructs that allow users to create a document with text, concatenate documents together, set indentation level, and express formatting choices. High-level constructs can then be built on top of the core constructs. The details of these core constructs can differ from PPL to PPL. We found that there are two main schools of PPLs in the wild, which we call the traditional and arbitrary-choice PPLs. The traditional PPL centers around manipulation of nls (newlines) and current indentation level, while the arbitrary-choice PPL is characterized by the ability to express arbitrary formatting choices and the use of aligned concatenation to supplant the concept of indentation level. Figure 1 illustrate documents in both PPLs that pretty-print the function definition append in a hypothetical programming language with slightly different styling.
Expressiveness. The literature contains informal claims about the expressiveness of PPLs [Chitil 2005; Podkopaev and Boulytchev 2015; Wadler 2003]. We develop two formal notions of expressiveness: the ability to express layouts and the ability to express features. The former reflects the functionality of a PPL, while the latter reflects the ease of document construction. Using our framework, we can show that neither the traditional PPL nor the arbitrary-choice PPL is more expressive than the other. For example, the set of layouts in Figure 1b cannot be expressed by any document in the traditional PPL. This is because all layouts due to a particular document in the traditional PPL must be the same modulo whitespace, but one of the layouts in the figure has an extra pair of parentheses.1 As another example, the document in Figure 1b is awkwardly constructed, because the document structure and the underlying AST structure do not match (Section 5.3). It would be more natural to use unaligned concatenation, but the feature cannot be expressed by any combination of features in the arbitrary-choice PPL.2 To that end, we develop a PPL called that is strictly more expressive than both the traditional and arbitrary-choice PPLs, facilitating both functionality and ease of document construction.
1Languages such as Python require an extra pair of parentheses around an expression that spans multiple lines [The Python Language Reference 2010]. Similarly, some styles prefer adding an extra comma (also known as trailing comma) when a function call spans multiple lines [ESLint 2014]. Hence, the ability to express layouts with differing content is desirable.
2Different programming language styles prefer different concatenation operators. C-like languages heavily use unaligned concatenation, while aligned concatenation has been used for Haskell, Lisp, R, and Julia. However, there are instances where C-like languages would benefit from aligned concatenation, and Haskell would benefit from unaligned concatenation.
text "function_append(first,second,third){" 1 function append(first,second,third){
<> nest 4 ( 2 return first +
let f = text "first_+" in 3 second +
let s = text "second_+" in 4 third
let t = text "third" in 5 }
nl <> text "return_" <>
group (nest 4 (f <> nl <> s <> nl <> t)) 1 function append(first,second,third){
) <> nl <> text "}" } 2 return first + second + third
3 }
(a) A document in the traditional PPL and its corresponding layouts. The nest construct increments the current indentation level by some specified amount, causing nl (newline) to insert indentation spaces. <> is the unaligned concatenation operator, which places the right sub-layout after the left sub-layout on the current indentation level. Lastly, the group construct creates a choice between two alternatives: one where the sub-layouts are left alone and one where the sub-layouts are flattened by replacing newlines and indentation spaces due to nls in the group with single spaces.
text "function_append(first,second,third){" <> 1 function append(first,second,third){
( let f = text "first_+" in 2 return (
let s = text "second_+" in 3 first +
let t = text "third" in 4 second +
let sp = text " " in 5 third
let ret = text "return_" in 6 )
text "..." <> 7 }
(((ret <> text "(") <>
(text "..." <> (f <> s <> t)) <>
text ")") <>
(ret <> f <> sp <> s <> sp <> t))
<> text "}" } 1 function append(first,second,third){
2 return first + second + third
3 }
(b) A document in the arbitrary-choice PPL and its corresponding layouts. <|> is the arbitrary-choice operator, which per its namesake, creates a choice between the layouts of two arbitrary sub-documents. <> is the vertical concatenation operator, which joins two sub-layouts with a newline. Lastly, <>> is the aligned concatenation operator, which joins two sub-layouts horizontally, aligning the whole right sub-layout at the column where it is to be placed in.
Fig. 1. The traditional and arbitrary-choice PPLs, embedded in the host language OCaml. Colored regions in a document and corresponding layouts indicate the correspondence between the colored sub-documents and the colored sub-layouts. We use the let construct to make the documents easier to read, even though it is usually not a part of PPLs. Dotted lines illustrate different page width limits at 22 and 36 characters.
Optimality. The optimality objective of a printer indicates what it optimizes for when resolving choices. Most printers targeting the traditional PPL minimize overflow past the page width limit line-by-line, preferring a longer line when there is no overflow. For example, given the document in Figure 1a, the first layout is optimal when the page width limit is 22 (red dotted line), while the second layout is optimal when the page width limit is 36 (green dotted line). Contrary to prior claims [Chitil 2005; Wadler 2003], we discovered that this strategy guarantees neither the absence of overflow whenever possible nor the minimality of the number of lines. By contrast, most printers targeting the arbitrary-choice PPL minimize the number of lines among layouts with no overflow. However, they error when all possible layouts have an overflow, resulting in a poor user experience (e.g., when the page width limit is 22 in Figure 1b). Recognizing that unavoidable overflows do occur in practice, we introduce the concept of a cost factory, which allows users to choose a desired objective permitted by its interface, including an objective that tolerates overflow gracefully.
Performance. Printing proceeds in two phases: resolving choices and rendering the optimal choice to text (although many printers fuse these two phases together). Time complexity of printers is best measured against the resolving phase3, and it is usually specified with two parameters: the size of the document and the width limit , with the preference that the time complexity be polynomial in and linear in . Most printers in the literature leave their time complexity unanalyzed, instead opting to show experimental results that their implementations are efficient in practice. We analyze these printers and demonstrate documents that trigger worse than linear time behavior (in ) on some printers. Further complication arises in printers with the arbitrary choice feature, which gives rise to documents that are structured as DAGs as opposed to trees. We show that many printers that treat the input document as a tree suffer from a combinatorial explosion as the DAG structure is unfolded during the resolving phase, resulting in exponential time complexity. With a combination of proof and experimental results, we show that the time complexity of is linear in the DAG size of the document and that it runs fast in practice.
In summary, this paper makes the following contributions:
- • A new PPL called that is strictly more expressive than all published PPLs. The constructs in are not new, but packaging them all in a single PPL has never been done before.
- • A printer targeting that utilizes a cost factory to allow a variety of optimality objectives.
- • A proof of correctness (validity and optimality) for , formalized in the Lean theorem prover. To our knowledge, this is the first time that a printer has been formally verified.
- • A framework to formally reason about the expressiveness of PPLs.
- • A survey of printers and an analysis that dispels common misunderstandings about them.
- • An implementation of , PRETTYEXPRESSIVE, and an evaluation that shows its effectiveness.
The rest of this paper is structured as follows. Section 2 surveys the related work. Section 3 provides an overview of from the user’s perspective. Section 4 presents the formal semantics of . Section 5 introduces a framework to reason about the expressiveness of PPLs. Section 6 formally presents and its analysis. Section 7 discusses PRETTYEXPRESSIVE, an implementation of . Section 8 presents an evaluation of PRETTYEXPRESSIVE that demonstrates its effectiveness. Lastly, Section 9 concludes the paper.
1.3. 2 RELATED WORK
To understand the trade-off space of printer designs, we conduct a comprehensive analysis of related work in the literature. This section provides our analysis of the printers, grouped by the expressiveness of their public interface4. The summary is presented in Table 1. We then compare and contrast our printer against them.
1.3.1. 2.1 Traditional Printers
Pretty printing has a long history. Oppen [1980] first introduced a general-purpose printer, written in the imperative style. Oppen pioneered the PPL that we call the traditional PPL, shown in Figure 2a. Instead of representing an input document as a tree, as commonly done in subsequent work, Oppen represents the document as a stream of “instruction tokens.” The algorithm’s time complexity is , where is the length of the stream. Furthermore, the algorithm is bounded, requiring a limited look-ahead into the stream. As with other printers in this family, the printer greedily
3This formulation allows us to talk about “linear-time” printers, even though there are, e.g., documents whose size is , but its optimal layout has characters.
4In practice, printers include extensions that increase their expressiveness. A printer may even have different expressiveness across different versions. This section focuses on the core features of these printers as specified in their publications.
| Printer | Expressiveness | Optimality | Performance | |
|---|---|---|---|---|
| Choice | Concatenation | Minimization objective | Time complexity | |
| Oppen [1980] | Group | Unaligned | Lexicographic overflow | |
| Hughes [1995] | Group | Aligned | Lexicographic overflow | |
| Wadler [2003] | Group | Unaligned | Lexicographic overflow | |
| Leijen [2000] | Group | Both | Lexicographic overflow | |
| Chitil [2005] | Group | Unaligned | Lexicographic overflow | |
| Kiselyov et al. [2012] | Group | Unaligned | Lexicographic overflow | |
| Swierstra et al. [1999] | Arbitrary | Aligned | Height† | Exp. in |
| Podkopaev and Boulytchev [2015] | Arbitrary | Aligned | Height† | |
| Yelland [2016] | Arbitrary | Aligned | Linear cost | |
| Bernardy [2017c] | Arbitrary | Aligned | Height† | |
| Both | Both | Cost (from the cost factory) | ||
| (aligned only) | Both | Aligned | Cost (from the cost factory) | |
† only consider layouts without an overflow past .
| text | text | ||
| newline | aligned concatenation | ||
| unaligned concatenation | vertical concatenation | ||
| increase the indentation level by | an arbitrary choice | ||
| a choice between flattening or not flattening |
(a) A variant of traditional PPL from Wadler [2003].
(b) A variant of arbitrary-choice PPL from Podkopaev and Boulytchev [2015].
minimizes overflow past the page width limit, which neither avoids overflow whenever possible nor minimizes number of lines, as discussed in Oppen’s paper.
Wadler [2003] designed a printer that targets the traditional PPL. It is used in many real world applications, such as an industrial code formatter [Prettier 2016], and as a basis for much pretty printing research [Chitil 2005; Kiselyov et al. 2012]. The printer aims to be a rewrite of Oppen’s printer using the functional style employed by Hughes (described later). The printer is claimed [Chitil 2005; Wadler 2003] to produce an output layout that does not exceed the width limit whenever possible, and minimizes the number of lines. However, this is not the case, as shown in Figure 16 in Appendix A. The time complexity of the printer is claimed to be where is the size of document [Wadler 2003], but it is in fact in the worst case, as demonstrated in Figure 17, although this worst case behavior is unlikely to occur in practice.
Chitil [2005] improved Wadler’s printer so that it is as efficient as Oppen’s, , by using lazy dequeues. Kiselyov et al. [2012] similarly improved Wadler’s printer via their generator framework.
Compared to traditional printers, is more expressive as it allows arbitrary choices and aligned concatenation. Furthermore, can produce an output layout that minimizes number of lines when the output layout does not exceed the page width limit, and does not exceed the page width limit whenever possible. The tradeoff is that is less space efficient and slower than traditional printers. The space complexity of traditional printers is sub-linear in the size of document, which was especially important decades ago when memory is scarce. The space complexity of is
let shared := D in (shared <> text "!" ) <|> ((shared <> nl) <> text "!")
(a) A document that encodes (at least) two possible layouts. is an arbitrary sub-document.
(b) A tree representation of Figure 3a. contributes to the size twice.
(c) A DAG representation of Figure 3a. contributes to the size only once.
Fig. 3. An example document that shows the importance of treating document as a DAG rather than a tree. The red and pink paths illustrate that the DAG is properly shared, as will be discussed in Section 6.4.
in the worst case (or when targeting some PPLs). We find that, on modern machines, the added memory consumption and performance overhead are rarely an issue in practice (Section 8).
1.3.2. 2.2 Arbitrary-Choice Printers
Azero Alcocer and Swierstra [1998] introduced a printer that supports aligned concatenation and choices between arbitrary alternatives. It started the line of work that targets the arbitrary-choice PPL, shown in Figure 2b. The printer’s optimality objective is to avoid overflow whenever possible and produce a minimal number of lines. However, it does not have the ability to cope with unavoidable overflow. This printer was soon superseded by Swierstra et al. [1999], which improves its performance via heuristics and adds the capability to share a sub-document across choices by deeply embedding the (equivalent of a) let construct in the PPL. As a result, the later printer can process documents that are structured as DAGs rather than trees, as shown in Figure 3. Nonetheless, the time complexity of both printers is exponential in [Podkopaev and Boulytchev 2015].
Podkopaev and Boulytchev [2015] improved upon Swierstra et al.’s work by formulating the problem as dynamic programming. This fixes the exponential blowup in the prior work, but treats the document as a tree, making its time complexity , where is the tree size of the document, which could be exponentially larger than its DAG size. The paper acknowledges the problem and surmises that memoization may be able to address it.
The paper by Bernardy [2017c] is the main inspiration for our work. The printer uses Pareto frontiers to find an optimal layout. By shallowly embedding the PPL (in Haskell), computations on sub-documents are effectively shared for free. However, as presented in the paper, the printer requires the page width limit to be hard-coded. In the actual implementation [Bernardy 2017b], the page width limit is customizable, accomplished by threading the value through functions. But this change destroys the shared computations, leading to exponential running time. Compared to Podkopaev and Boulytchev [2015]’s work, Bernardy [2017c]’s approach can exploit sparseness to improve practical efficiency, but the use of an inefficient algorithm makes the time complexity of the printer in the worst case. While the paper does not handle unavoidable overflow, the implementation does by automatically scaling up the page width limit (or equivalently, minimizing the maximum overflow). This, however, allows avoidable overflow elsewhere, as shown in Figure 18 in Appendix A, which is undesirable. Later on, Bernardy abandoned the arbitrary-choice operator, noting that it could trigger the exponential behavior [Bernardy 2017a].
Yelland [2016] similarly targeted the arbitrary-choice PPL. However, the paper took a very different approach. The core printer restricts the use of aligned concatenation by requiring the left sub-document to be a text syntactically. This restriction allows the core printer to utilize the concept of “piecewise linear cost function” to seemingly boost its performance. To achieve the expressiveness of the arbitrary-choice PPL, the printer employs rewriting rules to transform the original document into the restricted document. While the work carefully avoids exponential blowup by sharing sub-documents in the resulting restricted document, it does not necessarily preserve the sharing structure of the original document, as demonstrated in Figure 19 in Appendix A. Compound this with the lack of a computation width limit, and the number of piecewise linear cost functions under consideration could be as large as , making the time complexity in total, as shown in Figure 20. Another aspect to consider is the printer’s optimality objective, which is restricted to minimizing a linear combination of quantities like the number of lines and overflow. Hence, the work will not technically avoid overflow whenever possible (although the overflow coefficient can be made arbitrarily large to arbitrarily discourage overflow). On the other hand, this optimality objective can support unique features, such as incorporating the costs due to multiple soft page width limits.
Compared to arbitrary-choice printers, is more expressive as it allows unaligned concatenation. is also asymptotically faster than most arbitrary-choice printers, as it treats a document as a DAG rather than a tree. Like Yelland’s printer, for each layout under consideration, keeps track of two quantities: cost and last line length. This is different from most printers in the family which keep track of three quantities: height, width, and last width. The dimension reduction further makes more efficient. The concept of cost also allows to decouple the page width limit and computation width limit, which allows graceful overflow handling. , unlike Yelland’s printer, is parameterized by a cost factory, which supports a variety of optimality objectives without requiring a modification to the core printer. This includes not only the linear optimality objectives that Yelland’s printer supports, but also non-linear optimality objectives that can properly avoid overflows.
1.3.3. 2.3 Other Printers
Coutaz [1984] introduced one of the earliest document abstractions for user interfaces. The abstraction is very general: it can not only describe text layout, but also image and objects on computer screen. Due to its minimality, it is much less expressive than other printers for textual printing.
Hughes [1995] brought pretty printing to the functional world. The work pioneers using combinators to construct a document for pretty printing, which is now a standard practice. The printer targets a PPL that is neither the traditional nor arbitrary-choice PPL, but somewhere in-between. In particular, it only supports aligned concatenation and does not provide the arbitrary-choice operator in the public interface. The work is more similar to the traditional printers in how it makes choices greedily, which minimizes neither overflow nor number of lines. The combination of greedy choice making and aligned concatenation makes some documents print very poorly [Bernardy 2017c]. Furthermore, Peyton-Jones [1997] identified quadratic time complexity in the printer.
Leijen [2000] implemented Wadler’s printer in Haskell and added support for aligned concatenation via the inclusion of align, becoming the first printer that supports both aligned and unaligned concatenation. However, similar to Hughes’ printer, the printer can produce very poor output [Bernardy 2017c].
1.4. 3 AN OVERVIEW OF \Pi_e
takes as inputs a document in , a cost factory, and a computation width limit, and returns a textual layout. This section provides an overview of from the user’s perspective—what form the inputs take, and how they interact to produce a layout.
Document String without newline
| Cost type | |
| must be a total ordering (transitive, antisymmetric, and total) | |
| must be associative, with the identity that is | |
Fig. 6. The cost factory interface. Users need to supply the cost type and implement the operations satisfying the contracts indicated in the interface.
1 ←3→= func( pretty, print ) 2 pretty, 3 print 4 ) !
Fig. 7. Two example layouts to illustrate how a cost factory computes their costs. Both layouts are rendered at column position 3. The dotted lines shows the width limit of 6 and 14.
The nesting doesn't visibly increase the indentation level by 42. To see why, note that nest 42 ... is rendered at the column position 1 and indentation level 0. Subsequently, align ... is rendered at the column position 1 and indentation level 42. Then, text "b" <> nl <> text "c" is rendered at the column position 1 and indentation level 1. That is, the alignment on the inner document overrides the indentation level. This example shows the importance of the indentation level, and why it must be specifically tracked.
This concludes our internal description of how a choiceless document renders to a layout. General documents, by contrast, can contain the (arbitrary-) choice operator <|>, which provides a choice among the layouts from two sub-documents. Thus, unlike choiceless documents, which render to a single layout, general documents will evaluate to a non-empty, finite set of layouts. Intuitively, this is done by first widening a document into a set of choiceless documents, then rendering each choiceless document in the set, producing a set of layouts.
Example 3.3. The document (text "a" <|> text "b") <> (text "c" <|> text "d") widens to four choiceless documents: text "a" <> text "c", text "a" <> text "d", text "b" <> text "c", and text "b" <> text "d". Thus, the document evaluates (with column position and indentation level 0) to a set of four layouts: "ac", "ad", "bc", and "bd".
1.4.1. 3.2 Cost Factory
To pick an optimal layout from the set of layouts that a document evaluates to, needs to be able to compute a cost for each layout, and to compare these costs to find a layout with minimal cost. To accommodate a wide range of optimality objectives, we allow the user to specify a cost type and implement operations on the cost type:
- • a procedure that computes the cost of text starting at column of length
- • a constant that gives the cost of a newline5
- • a procedure that adds two costs together
- • a procedure that compares two costs
We call this set of parameters a cost factory. These parameters cannot be arbitrary, however. For example, the cost of "hello_world" placed at column 10 should be the same as the cost of "hello_"
5In our Lean formalization and actual implementation, is a procedure. See Section 7 for details.
placed at column 10 combined with the cost of “world” placed at column 16.6 Thus, a valid cost factory also needs to additionally satisfy the contracts listed in Figure 6.7 The first three contracts allow to efficiently prune away suboptimal costs during incremental cost computation (Section 6), and the last three contracts ensure that the concept of the cost for a layout is well-defined.
With a cost factory, we can inductively compute the cost of a layout with lines rendered at column position :
can then use to find an optimal layout. The following example shows a concrete cost factory and how it can be used to pick an optimal layout among the layouts in Figure 7.
Example 3.4. Consider an optimality objective that minimizes the sum of overflows (the number of characters that exceed a given page width limit in each line), and then minimizes the height (the total number of newline characters, or equivalently, the number of lines minus one). This objective is thus able to avoid the excessive overflow problem in Bernardy’s printer described in Section 2.
More concretely, the cost of a layout is a pair of the overflow sum and the height, where lexicographic order determines which cost is less. With , the first layout in Figure 7 has the cost , whereas the second layout has the cost . Thus, the second layout is the optimal layout that should pick.
We implement this optimality objective with the following cost factory .
According to , the first layout has cost , while the second layout has the cost , as expected.
The cost factory interface is versatile. The above example shows that does not need to take a page width limit as an input, because the concept of page width limit can already be defined by users via . It is also possible, for example, to implement soft width limits, or to compute a linear combination of height and overflow in the style of Yelland [2016]. The rest of this section provides a couple more examples of other valid and invalid cost factories.
Example 3.5. The following cost factory targets an optimality objective that minimizes the sum of squared overflows over the page width limit , and then the height. This optimality objective is an improvement over the one in Example 3.4 by discouraging overly large overflows. With , the first layout in Figure 7 has the cost whereas the second layout has the cost The text cost formula is derived from the identity where in each text placement, is the starting position count past and is the overflow length. This is (essentially) the default cost factory that our implementation, PRETTYEXPRESSIVE, employs.
6In other words, the cost of a long text should be able to be broken down into the costs of its characters.
7For mathematical readers, a (valid) cost factory forms a totally ordered monoid with translational invariance.
Example 3.6. The following cost factory targets an optimality objective that minimizes the maximum overflow over the page width limit . With , the first layout in Figure 7 has the cost 20 whereas the second layout has the cost .
The above cost factories are all valid. This is proven with automated theorem proving via Rosette 4 [Porncharoenwase et al. 2022; Torlak and Bodik 2014] and Z3 [De Moura and Bjørner 2008].
THEOREM 3.7. The cost factories in Example 3.4, Example 3.5, and Example 3.6 are valid.
Example 3.8. The following invalid cost factory intends to target an optimality objective that minimizes the maximum overflow over the page width limit , and then the height. However, the second contract is violated, because does not hold.
1.4.2. 3.3 \mathcal{W}, the Computation Width Limit
The last input to is , the computation width limit. When printing a document , only provides the optimality guarantee among layouts evaluated from whose column position or indentation level during the printing process does not exceed . For each choiceless document widened from , when its rendering causes a column position or indentation level to exceed the computation width limit, the rendering is tainted. For example, if a document evaluates to two layouts in Figure 7, with , the rendering to the first layout would be tainted, while the rendering to the second layout would not (assuming the indentation level during the rendering doesn't exceed the limit). Layouts from tainted rendering can usually be discarded right away, except when every possible rendering is tainted. In such case, keeps one layout so that it can still output a layout, but provides no guarantee that the layout will be optimal. The tainting system allows us to bound the computation so that the algorithm is efficient.
1.5. 4 THE SEMANTICS OF \Sigma_e
This section formally presents , an expressive PPL. We begin this section by describing layouts, which are the textual outputs. Then, we formally describe the semantics of , which is determined by the evaluation of a document in to a set of layouts.
1.5.1. 4.1 Layouts
A layout is a textual output. We represent a layout as a non-empty, finite list of lines (implicitly joined by newlines), where each line is a string without the newline character.8 This allows us to easily reason about the number of lines and the length of each line. The first line of a layout can be put at an arbitrary column position (depending on which column position it is rendered at), but subsequent lines must be put at the column position 0.
Example 4.1. The second layout in Figure 7 is ["func(", "_pretty,", "_print", ")"], which is rendered at the column position 3.
8The representation in our Lean formalization is more elaborated, making indentation level explicit by incorporating the information into a layout. We present a simplified version here for the sake of simplicity. See Section 7 for details.
Fig. 8. Semantics for . “” is the empty string. “” is the notation for replicating the string for times. “” is a string concatenation of and . Lastly, “” and “” indicate lines, where and respectively.
1.5.2. 4.2 The Formal Semantics of \Sigma_e
Our approach to evaluate a document in to a set of layouts is to first widen a document into a set of choiceless documents, then render each choiceless document in the set, producing a set of layouts.
The formal semantics of consists of two relations, shown in Figure 8. The judgment states that the choiceless document placed at column position with indentation level and flattening mode , will render to the layout . Unlike the informal semantics in Section 3.1, we make the flattening mode , which indicates whether newlines should be replaced with spaces, explicit. Its value can be either on () or off (). Another judgment states that a document is widened to a finite, non-empty set of choiceless documents . We sometimes call a combination of and (and possibly ) a printing context. Now, we elaborate some interesting rules in the figure.
Rendering Text. The TEXT rule states that the rendering of a text placement text contains a layout with a single line of the text . The printing context is completely ignored.
Rendering Newlines. When the flattening mode is off, the LINENOFLATTEN rule states that the rendering of a nl results in a layout with two lines. The first line is empty, while the second line is indented by spaces. On the other hand, when the flattening mode is on, the LINEFLATTEN rule states that the rendering of the newline results in a layout with a single line of a single space.
Rendering Unaligned Concatenation. In the rendering of , we recursively render and , but the rendering of is dependent on the rendering of . Let be the rendering result of . The CONCATONE rule handles the case where has a single line, and the CONCATMULT rule handles the case where has multiple lines.
- • If has only a single line , the column position of 's rendering needs to be after the string is placed, i.e. at . In such case, let be the rendering result of . The first line of the resulting layout is the concatenation of and the first line of . The rest of the lines are from the rest of .
- • On the other hand, if has multiple lines, the column position of 's rendering is simply the column position after the last line is placed. In such case, let be the rendering result of , the resulting layout contains all but the last line of , a concatenation of the last line of and the first line of , and the rest of .
Widening Choices. The UNIONWIDEN rule states that the widening of is the union of widen and widen .
Both and are deterministic and total. Thus, we can define as the evaluation function for , which consumes a document, widens it, and produces a set of layouts.
1.6. 5 A FRAMEWORK TO REASON ABOUT EXPRESSIVENESS
In previous sections, we informally made claims about expressiveness of PPLs. This section presents a framework to formally reason about it, based on two notions: functional completeness and definability. We first define the semantics of the traditional and arbitrary-choice PPLs. Then, we define our framework, and show that is strictly more expressive than both the traditional and arbitrary-choice PPLs while being minimal.
In particular, Theorem 5.12 states that every construct in the traditional and arbitrary-choice PPLs is definable in . However, Theorem 5.17 states that some of these constructs are not definable in the traditional and arbitrary-choice PPLs. Finally, Theorem 5.19 shows that is minimal. Proof sketches of theorems in this section are provided in Appendix B.
1.6.1. 5.1 The Extended Semantics
To reason about the traditional and arbitrary-choice PPLs, we need to precisely define their semantics. To do so, we construct a PPL that contains all constructs from and the traditional and arbitrary-choice PPLs by extending Figure 8 with Figure 9 (along with the straightforward widening rules). Note that we follow Wadler [2003]'s approach by treating group as a syntactic sugar for . As and flatten are already in , we do not need to adjust anything further.
The extended semantics are still deterministic and total. The semantics of the traditional and arbitrary-choice PPLs is then the restricted semantics of that only allows their constructs.9 Throughout this section, we assume that any PPL is similarly a sublanguage of , whose semantics is well-defined and consistent with .
9It is worth noting that there are many ways to specify rules to be consistent with the intended semantics of the arbitrary-choice PPL. For instance, an invariant in the the arbitrary-choice PPL is that throughout the rendering process. As a result, we could substitute the VERTCONCATNOFLATTEN rule with its variant that changes the premise to , without affecting the semantics of the arbitrary-choice PPL. However, this change could affect the semantics of and subsequent theorems in this section. We pick VERTCONCATNOFLATTEN over the variant because it seemingly integrates better with other constructs in .
Fig. 9. The semantics extension.
1.6.2. 5.2 Functional Completeness
In Section 1, we claimed that the traditional PPL cannot express the two layouts in Figure 1b, as one layout has an extra pair of parentheses. The question that we may want to ask in general then is, given a PPL and a non-empty set of layouts , is it possible to construct a document in that evaluates to ? This motivates us to define the notion of functional completeness for PPLs.
Definition 5.1. A PPL with an evaluation function is functionally complete if for any non-empty set of layouts , there exists a document in such that .
With this definition, we can formally reason about some PPLs that we have previously seen.
LEMMA 5.2. The arbitrary-choice PPL and are functionally complete.
LEMMA 5.3. The traditional PPL is not functionally complete.
LEMMA 5.4. For each construct in , without is not functionally complete.
If we limit the notion of expressiveness to only functional completeness, then all functionally complete PPLs would be equally expressive. However, intuitively this is clearly not the case. The proof of Lemma 5.2 in Appendix B shows that it suffices for a PPL to only have text, <>, and <|> for functional completeness, yet such a PPL would not be pleasant to use compared to , because of the lack of features to, e.g., adjust indentation level. In a sense, functional completeness for PPLs is similar to Turing completeness for programming languages, which similarly does not fully capture expressiveness for programming languages. The next subsection presents a more fine-grained notion of expressiveness, based on the ability to define features.
1.6.3. 5.3 Definability
The proof of Lemma 5.2 shows that while doesn't have <>, we can simply expand to , which are in , to perform the same functionality. In other words, the construct <> is already definable by <> and n1. Thus, adding <> to doesn't increase its expressiveness. By contrast, <> is not definable by any combination of features in the arbitrary-choice PPL. To achieve the functionality of <>, it would require a non-local restructuring of the document, making it difficult to construct the document in natural way. In this sense, the inability to define a construct in a PPL means that adding the construct to the PPL increases its expressiveness.
More concretely, consider the document in the arbitrary-choice PPL shown in Figure 1b. The document is awkwardly constructed. The return keyword must be distributed to combine with a first line of the returned expression, due to the undefinability of unaligned concatenation. This creates a disconnection between the document structure and the underlying AST structure, making it more tedious and error-prone to construct documents. By contrast, the following document is a rewrite of Figure 1b to utilize the full expressiveness of in a natural way. The sub-document colored blue fully corresponds to the “returned expression,” allowing users to recursively construct documents naturally.
text "function_append(first,second,third){" <> nest 4 (
let f = text "first_+" in let s = text "second_+" in let t = text "third" in
nl <> text "return_" <>
((text "(" <> (nest 4 (nl <> (f <> nl <> s <> nl <> t))) <> nl <> text ")") <|>
let sp = text "_" in (f <> sp <> s <> sp <> t))
) <> nl <> text "}"
The notion of definability (also known as expressibility) for programming languages was first developed by Felleisen [1991], and we adapt it for PPLs through a series of definitions as follows:
Definition 5.5. A PPL consists of:
- • a set of (possibly infinitely many) function symbols . The function symbols are referred to as constructs. Each may have different arity, argument sorts, and resulting sort.
- • a non-empty set of documents generated from , where a document is a term of sort Doc.
- • an evaluation function .
Example 5.6. contains nest, which is a construct with arity 2 of resulting sort Doc. The first argument to nest has sort and the second argument has sort Doc. also contains all natural numbers and strings with no newline, which are constructs with arity 0 of resulting sort and Str respectively. The evaluation function for is from Section 4.2.
Henceforth, unless indicated otherwise, and are the set of documents and the evaluation function for the PPL .
Definition 5.7. A syntactic abstraction of arity for a PPL is a document in where are metavariables of some sorts. An instance is a document in that substitutes with in for all , where and must have a compatible sort.
Example 5.8. is a syntactic abstraction for , where and have sort Doc. On the other hand, is not a syntactic abstraction because the first occurrence of requires it to have sort , but the second occurrence requires it to have sort Doc. An instance is the document , but is not an instance due to the incompatible sort.
Definition 5.9. Let be a PPL and where has arity with resulting sort Doc. A syntactic expansion from to is a function from to that replaces every occurrence of with an instance in , where and must have compatible arity and sort arguments.
Example 5.10. is a syntactic expansion from to , where is from Example 5.8. Hence, .
We are now ready to define definability.10
10One important distinction of this definition and Felleisen’s counterpart is that PPLs are total. Hence, observing termination behavior, as done in Felleisen’s work, is not feasible in our formulation.
Definition 5.11. Let be a PPL and . We say that is definable by if there exists a syntactic abstraction from to such that for every document , .
We can now present one of our main results:
THEOREM 5.12. Every construct in the traditional and arbitrary-choice PPLs is definable in .
Despite the result, one might wonder if is actually needed. Could it be that the arbitrary-choice PPL can already define every construct in the traditional PPL? As we foreshadowed, the answer to this question is negative. However, we must first develop tools that allow us to answer the question, again following the development in Felleisen’s work.
Definition 5.13. A context for is a unary syntactic abstraction for where has sort Doc.
Definition 5.14. Given a PPL and a relation , the relation holds if and only if holds for all contexts in .11
Example 5.15. Let be a function that computes the maximum length across all lines in the input layout, and lift to work on any set of layouts. That is, . Furthermore, let .
- • holds by induction. Intuitively, this is because (1) if we only observe the width, the textual content doesn’t matter, and (2) there is no construct in that allows us to lay out differently in a way that would affect the width based on the textual content.
- • On the other hand, . For example, with , we have that , but .
The following theorem provides a tool to prove that a construct is not definable in a PPL.
THEOREM 5.16. Given a PPL and a construct , if there exists two documents and in and a relation such that , but , then is not definable in .
With this tool, we are able to prove that some constructs of are not definable in the traditional and arbitrary-choice PPLs:
THEOREM 5.17. The following is true:
- • is not definable in the arbitrary-choice PPL.
- • nest is not definable in the arbitrary-choice PPL.
- • group is not definable in the arbitrary-choice PPL.
- • is not definable in the traditional PPL.
Next, we show a relationship between functional completeness and definability.
LEMMA 5.18. If is not functionally complete, but is, then is not definable in .
Lastly, we present our final result for this section: is minimal in the sense that each of its constructs is not definable by without it.
THEOREM 5.19. For any construct of , is not definable in .
11The relation is a generalization of the operational equivalence relation in Felleisen’s work.
Fig. 11. Relationship between evaluation and printing
1.7. 6 OUR PRINTER, \Pi_e
In this section, we describe our printer, , which targets the PPL presented in Section 4. is parameterized by a cost factory and a computation width limit . We start with an overview of . Then, we define a measure, which is an output from the core printer that allows us to record a cost and at the same time avoid a full-blown, expensive rendering. After that, we describe the requirements of the input document structure, which will become important when we analyze the time complexity of the printer. Then, we present 's printing algorithm, which utilizes the cost factory to achieve optimal and efficient printing. Finally, we analyze the time complexity of .
1.7.1. 6.1 Overview
So far we have defined the evaluation of a document, which produces the set of possible layouts. But when we print a document, we wish to output only a single, optimal layout.
A naïve approach would be to evaluate the input document, via widening and rendering, to all possible layouts, determine costs of these layouts according to a given optimality objective, and then pick one with the least cost as the optimal layout. However, this approach is not practical for two reasons. First, widening could produce exponentially many choiceless documents. Second, rendering non-optimal choiceless documents is unnecessary and wasteful.
A better approach would utilize early pruning to reduce the search space, and avoid rendering until an optimal choiceless document is first identified. The need to prune early motivated the design of the cost factory interface shown in Figure 6, which allows to incrementally compute
Fig. 12. Measure and operations on measures
Fig. 13. Measure computation from a choiceless document in a printing context
costs to be used for pruning decisions. Since we wish to avoid full-blown rendering, we will instead operate on measures [Bernardy 2017c], which record the information about a choiceless document required for pruning without expensive rendering.
The workflow of is shown in Figure 10, while Figure 11 shows how it relates to the evaluation of a document. The printer first resolves choices, with early pruning, to produce a small set of measures that contain the optimal measure. The set in particular forms a Pareto frontier in the cost and last line length trade-off (Section 6.2 and Section 6.3). We then pick the optimal measure from the set and render its choiceless document to produce an optimal layout.
In the rest of this section, every definition and theorem is implicitly parameterized by a cost factory and a computation width limit .
1.7.2. 6.2 Measure
As presented earlier, the resolving phase computes measures. Presented in Figure 12, a measure consists of five components: length of last line (), cost (), choiceless document (), max column position (), and max indentation (). We gray out the last two components because they are ghosted [Owicki and Gries 1976]: they are only needed for the correctness theorem, and not required in the actual implementation.
Example 6.1. Let be the choiceless document in Example 3.1. With the cost factory in Example 3.4 and , the choiceless document is rendered at the column position 3 and indentation level 0 to the second layout in Figure 7, with the cost (8, 3). The column position of the last line is 1. The maximum column position attained is 10 (on the first line), and the maximum indentation level attained is 2. Thus, the computed measure is .
Figure 13 shows rules that define measure computation. The judgment states that when we compute the measure of placed at the column position with indentation level , the resulting measure is . To simplify the core printer, we (temporarily) remove flatten from . This allows us to eliminate the flattening mode parameter, which implicitly defaults to . Toward the end of this section, we will show how to add support for flatten back.
The rules are largely standard. They reflect the actual rendering defined by , and utilize the cost factory in a straightforward way. The rules use a helper operator function to concatenate two measures, and helper functions adjustNest and adjustAlign to construct a correct measure for nest and align. These functions are defined in Figure 12. Notably, the LINEM rule creates a measure whose is because before placing the newline, the column position is , and after placing the newline, the column position is . The ALIGNM rule creates a measure whose is where is obtained via the recursive computation. This is because the recursive computation discards the current indentation level, so we need to specifically record the information.
is deterministic and total. It is also correct with respect to .
THEOREM 6.2. For any and , there exists a maximum indentation such that
- • if , then .
- • if , then
So far, we have only considered the measure computation for a choiceless document. When we take the choice operator into account, there could be multiple measures under the same printing context. The main operation that we can perform on these measures is finding domination , also presented in Figure 12. when both the cost and the last length of are no worse than those of . The fact that is useful because it allows us to prune away immediately.
1.7.3. 6.3 Measure Set
Resolving a document (in a printing context) produces a small set of measures. To accommodate taintedness mentioned in Section 3.3, Figure 14 defines a measure set to be either a non-empty Set of untainted measures where no measure dominates the other, or a Tainted singleton set of a promise that can be forced to a measure. The Set, by definition, forms a Pareto frontier. To aid computation, we represent the Set with a list ordered by the cost in strict ascending order (and therefore the last length in strict descending order). We are able to do so because in a Pareto frontier, all last and cost values must be distinct.
The main operation that we can perform on measure sets is merging two measure sets (), shown in Figure 14, where we prefer a Set over a Tainted. The merge operation maintains the Pareto frontier invariant, by doing the merge in the style of the merge operation in merge sort, although the Pareto frontier merging can also prune measures away during the operation. One important “quirk” of this merge operation is that it is left-biased in the presence of taintedness. If two tainted measure sets are merged, the result is always the left one. This means the order of arguments to the merge operation is important, as we will see in the next subsections.
Other operations on measure sets which are used in subsequent sections are taint, lift, and dedup. taint taints a measure set. When tainting a Set, we choose to pick the first measure from the Set because it has the least cost, which is a greedy heuristic. lift adjusts measures in a measure set. Lastly, dedup prunes measures that are sorted by last in strictly decreasing order and by cost in non-strictly increasing order, so that the result conforms to the Pareto frontier invariant.
| Measure set | where is a promise that can be forced to a measure |
| where and | |
| if if | |
Fig. 14. Measure set and the merge operation on measure sets. @ denotes a list concatenation. We treat a promise and a measure interchangeably, as they can be straightforwardly casted to each other.
1.7.4. 6.4 The Document Structure
Section 2.2 showed that we need to handle document sharing by treating the input document as a DAG. However, documents cannot be arbitrarily shared, as the following example shows:
Example 6.3. The following document has a DAG size of . However, resolving it necessitates units of computation, as the printing contexts are all different. This is bad news because it means that resolving could take time exponential in the input size.
let rec mk (n : int) : doc = if n = 0 then text "x" else let shared = mk (n - 1) in shared <> shared
However, we argue that the above document is not properly shared, because the sub-documents are not shared across choices, which is how sharing is employed in practice. The corresponding properly shared document would have DAG size, so units of computation are still linear in the input size. To make this precise, we provide the following definitions:
Definition 6.4. Given a document , is a DAG rooted at whose edge in the graph connects a document to its direct subdocuments.
Definition 6.5. A document is properly shared if for any two vertices and in , if and are two distinct paths from to , then there exists a common document such that (1) is a ; (2) occurs in both and ; and (3) is not .
Figure 3c shows a properly shared document (assuming that is properly shared). It illustrates two paths where is the root node, is , and is . In practice, non-properly shared documents can still be processed by , and in fact can even make resolving faster when a shared document is resolved under the same printing context. However, this shared document would be effectively
Fig. 15. The resolver
duplicated when it is resolved in different contexts. For simplicity, we only consider properly shared documents as the input to in this paper.
1.7.5. 6.5 The Resolver
We now formally define the core of , which is the resolver. It is described in Figure 15, which is a fusion of widening in Figure 8 and measure computation in Figure 13, with early pruning inherent in the merge operation and extra bookkeeping for taintedness. The judgment states that a properly shared document at a column position with an indentation level resolves to a measure set .
Resolving Text. If placing the text would exceed or the indentation level is beyond , the TEXTRSTNT rule returns a Tainted. Otherwise, the TEXTRS rule returns a singleton Set.
Resolving Newlines. Resolving a nl is similar to resolving a text, but we only need to consider the current column position and indentation level, as resolving the newline does not change the column position. The LINERSTNT and LINERS rules cover these two cases.
Resolving Nesting. Resolving a nest is handled by the NESTRS rule, which recursively resolves its sub-document with the indentation level changed. The recursive resolving determines whether the measure set will be a Set or Tainted. In all cases, the result is adjusted to construct correct choiceless documents.
Resolving Alignment. Resolving an align is similar to resolving nest. However, because the recursive resolving discards the current indentation level, which could exceed , we need to taint the measure set when the indentation level is beyond . The ALIGNRSTNT rule handles such cases, and the ALIGNRS rule handles the other possibilities.
Resolving Choices. The UNIONRS rule recursively resolves its two sub-documents and then merges the resulting measure sets. As mentioned in Section 6.3, the merge operation is left-biased. Therefore, the left sub-document will be preferred over the right sub-document if exceeding is unavoidable. It is possible to employ a heuristic to remove this bias, as discussed in Appendix C.
Resolving Unaligned Concatenation. Resolving a is done through the CONCATRSTNT and CONCATRS rules, which handle the two possibilities of measure set types obtained from the left sub-document’s recursive resolving. Notably, the CONCATRS rule employs to help us concatenate a left measure from the left measure set with a right measure set.
is deterministic and total. This allows us to define the top-level printer as where and , which consumes a properly shared document , resolves it to a set of measures, picks the measure with the least cost, and renders the associated choiceless document to produce a layout. (Our implementation further fuses resolving and rendering together, as described in Appendix C.)
While the rules above are enough for correctness, implementing these rules requires further consideration. As we will see in Lemma 6.9, any resolving beyond would eventually result in a tainted measure set. Hence, should immediately delay the computation for any resolving beyond . should also memoize the computation, so that on identical documents and printing contexts within , the result of the previous computation is reused.
We claim that consumes a properly shared document in and produces an optimal layout among within . We prove this claim in the next subsection.
1.7.6. 6.6 Correctness of \Pi_e
is correct with respect to . Two theorems govern the correctness. The first theorem states that the core printer returns a measure set that contains a measure that is no worse than any measure within the computation width limit from all possible measures.
THEOREM 6.6 (OPTIMALITY). For any , , , if the following conditions hold
- •
- •
- •
- •
- •
- •
then . Furthermore, there exists such that .
The second theorem states that measures in the resulting measure set are valid.
THEOREM 6.7 (VALIDITY). For any , , with , if , then for each , there exists such that and . Likewise, if , then there exists such that and .
The correctness of follows immediately.
While the above theorems guarantee the correctness of the result that the printer produces, they do not guarantee efficiency. The following lemmas provide some properties of the printer that allow us to reason about its efficiency.
LEMMA 6.8. For any , , , if , then .
LEMMA 6.9. For any , if or and , then is a Tainted.
We now informally prove the efficiency of that we claimed in Section 1. The proof sketches are provided in Appendix B.
THEOREM 6.10. The time complexity of is where is the DAG size of the document.
THEOREM 6.11. If a document is in the arbitrary-choice PPL, can print in .
1.7.7. 6.7 Handling Flattening
To support flatten, we make it a function that walks its sub-document and replaces all nl with text "_". The walk is memoized and preserves the original identity of the document whenever possible (i.e. if nothing is flattened in sub-documents, then the document itself is returned unchanged without creating a new document). Thus, each document can be flattened at most once. This flattening creates at most new documents without destroying the shared structure in the original document. We therefore achieve the functionality of flatten without affecting the time complexity of the printer.
1.8. 7 IMPLEMENTATION
We implement in OCaml and Racket. The printer, which we call PRETTYEXPRESSIVE, is further refined to be more efficient and practical. PRETTYEXPRESSIVE also includes more practical constructs that do not fit well to the formalism in this paper. We describe these refinements and additional constructs in Appendix C. The OCaml PRETTYEXPRESSIVE, as a reference implementation, is used for comparing against other printers in Section 8. The Racket PRETTYEXPRESSIVE has more features, and it has been used to implement a code formatter for the Racket programming language.
In these implementations, we extend the cost factory interface in Figure 6 so that is now a procedure that takes an indentation level as an input, and returns the cost of a newline along with indentation spaces, with a contract that . That is, was not customizable before, but it is now customizable.12 PRETTYEXPRESSIVE then provides a pre-defined cost factory that is like Example 3.5, but with .
1.9. 8 EVALUATION
This section evaluates the performance and optimality of PRETTYEXPRESSIVE. The evaluation consists of two parts. First, we compare PRETTYEXPRESSIVE against Wadler/Leijen [2000] and Bernardy [2017b]’s printers, which are popular practical printers with capabilities from the traditional and arbitrary-choice PPLs. Second, we evaluate the Racket code formatter, which uses PRETTYEXPRESSIVE as its foundation. The evaluation aims to answer the following questions:
- (1) Does PRETTYEXPRESSIVE run fast in practice?
- (2) Does PRETTYEXPRESSIVE produce pretty layouts in practice?
All experiments are performed on an Apple M2 MacBook Pro with 16GB of RAM. We describe the experiments and benchmarks in Section 8.1 and Section 8.2, and discuss the results in Section 8.3.
1.9.1. 8.1 Comparison of Printers
We compare OCaml PRETTYEXPRESSIVE against the latest version (1.2.1) of Wadler/Leijen’s printer, and the “camera ready version” of Bernardy’s printer13. This “camera ready version” consists of two printers: the “naïve” variant, which is presented in the paper, and the “practical” implementation, which has more features (such as unavoidable overflow handling) but suffers from exponential
12This change requires adjustments to many definitions and theorems, and we have done so for our Lean formalization. For example, to make Theorem 6.2 hold, we need to keep indentation spaces in the definition of layouts (Section 4.1).
13We also tried other versions of Bernardy’s printer, such as the commit 006fa0e8, which is the version right before the <|> operator was removed, and supposedly more optimized than the camera ready version. Unfortunately, we find that it has a severe performance deficiency. When attempting to replicate the experiments in Bernardy [2017c], we find that formatting the 10k-line-JSON file takes about 80 seconds, which is much slower than the 145 milliseconds reported in the paper.
Table 2. Comparison between PRETTYEXPRESSIVE in different configurations and other printers. For each printer and configuration, the first column reports the running time, and the second column reports the line count of the output layout. PRETTYEXPRESSIVE has an additional third column, where ✓ indicates that the rendering to the output layout fits and ✗ indicates that the rendering to the output layout is tainted. “N/A” means the benchmark is not applicable. ⌘ indicates that running the benchmark exceeds the timeout of 60 seconds. “-” means the data is not collected. A grayed row indicates an output mismatch among the printers/configurations. The bolded line count signals that in our manual inspection, the associated layout is the prettiest.
| Benchmark | PRETTYEXPRESSIVE | Wadler/Leijen | Bernardy | |||||
|---|---|---|---|---|---|---|---|---|
| default | (usually 100) | Naïve | Practical | |||||
| Concat10k | 0.000 s | 1 ✗ | 0.000 s | 1 ✗ | 0.002 s | 1 | N/A | 0.433 s |
| Concat50k | 0.002 s | 1 ✗ | 0.002 s | 1 ✗ | 0.011 s | 1 | N/A | 14.626 s |
| FillSep5k | 0.010 s | 668 ✓ | 0.010 s | 668 ✓ | 0.004 s | 668 | 3.097 s | 668 |
| FillSep50k | 0.190 s | 6834 ✓ | 0.190 s | 6834 ✓ | 0.035 s | 6834 | ⌘ | ⌘ |
| Flatten8k | 0.018 s | 7986 ✓ | 0.016 s | 7986 ✓ | 3.346 s | 7986 | N/A | N/A |
| Flatten16k | 0.036 s | 15986 ✓ | 0.037 s | 15986 ✓ | 18.816 s | 15986 | N/A | N/A |
| SExpFull15 | 3.027 s | 4107 ✓ | 5.437 s | 4107 ✓ | 0.045 s | 4107 | 0.647 s | 4107 |
| SExpFull16 | 5.255 s | 8246 ✓ | 14.232 s | 8246 ✓ | 0.091 s | 8246 | 1.251 s | 8246 |
| RandFit1k | 0.100 s | 629 ✓ | 0.229 s | 629 ✓ | 0.003 s | 943 | 0.048 s | 629 |
| RandFit10k | 1.047 s | 7861 ✓ | 4.420 s | 7861 ✓ | 0.037 s | 10459 | 0.534 s | 7861 |
| RandOver1k | 0.058 s | 1531 ✗ | 0.904 s | 1531 ✗ | 0.005 s | 1635 | N/A | 0.065 s |
| RandOver10k | 0.405 s | 15027 ✗ | 16.553 s | 15027 ✗ | 0.108 s | 16015 | N/A | 1.103 s |
| JSON1k | 0.001 s | 564 ✓ | 0.001 s | 564 ✓ | 0.003 s | 564 | N/A | 0.005 s |
| JSON10k | 0.007 s | 5712 ✓ | 0.007 s | 5712 ✓ | 0.018 s | 5712 | N/A | 0.097 s |
| JSONW | 0.001 s | 721 ✗ | 0.001 s | 721 ✓ | 0.002 s | 721 | N/A | 0.005 s |
Table 3. The code formatter benchmarks. The table is in the same format as the PRETTYEXPRESSIVE column in Table 2.
| Benchmark | Benchmark | ||||||||
|---|---|---|---|---|---|---|---|---|---|
| class-internal | 0.325 s | 5750 ✗ | 0.307 s | 5751 ✓ | list | 0.025 s | 993 ✓ | 0.025 s | 993 ✓ |
| xform | 0.372 s | 5154 ✗ | 0.417 s | 5154 ✓ | hash | 0.020 s | 83 ✓ | 0.020 s | 83 ✓ |
time complexity when the DAG structure unfolds, as discussed in Section 2. We manually remove the capability to customize the width limit from the latter to avoid the issue. Both variants are used for the evaluation, since the naïve variant does not have necessary features for some benchmarks.
PRETTYEXPRESSIVE is instantiated with the cost factory in Section 7, with a page width limit of 80 (unless indicated otherwise). We run PRETTYEXPRESSIVE twice with different computation width limits (once with , unless indicated otherwise, and once with ), in order to observe the effect of the tainting system and how it affects the performance.
The benchmarks (Table 2) are mostly taken from Bernardy [2017c], and we add a few more to test basic constructs. While Leijen’s printer is expressive enough to handle all benchmarks (due to the inclusion of align to support aligned concatenation in addition to constructs from the traditional PPL), Bernardy’s printers are not applicable to benchmarks that require constructs from the traditional PPL. Furthermore, Bernardy’s naïve printer is not applicable to benchmarks that require extra features like unavoidable overflow handling.
In more detail, the benchmarks test the following kinds of documents:
Concat benchmarks test a long chain of concatenations, which are identified by Peyton-Jones [1997] as a source of quadratic time complexity in Hughes’ printer.
FillSep benchmarks test the fillSep construct (also known as fill), which performs word wrapping.
Flatten benchmarks test repeated flattening, as shown in Figure 17 in Appendix A.
SExpFull benchmarks are the last two data points from the “full tree” benchmark in Bernardy [2017c]’s paper. They create complete binary trees and print them as S-expressions.
RandFit benchmarks [Bernardy 2017c] are similar to SExpFull, but use random Dyck paths to generate random trees and filter only those that fit within the page width limit.
RandOver benchmarks are like RandFit with the opposite filtering.
JSON benchmarks are also from Bernardy [2017c]’s paper. They format large JSON files.
JSONW benchmark is the same as JSON1k but with a page width limit of 50 instead of 80, and we further adjust PRETTYEXPRESSIVE’s default from 100 to 60 to test the tainting system.
1.9.2. 8.2 Racket Code Formatter
We evaluate the effectiveness of a Racket code formatter that uses the Racket PRETTYEXPRESSIVE as its foundation. Racket [Felleisen et al. 2018] is a programmable programming language. Its main syntax is S-expression based, but this can be customized via its #lang protocol to read an arbitrary syntax. Even in the S-expression syntax, users can define custom forms via the macro system. Our long-term plan for the code formatter is to make it extensible to support any syntax and custom forms. PRETTYEXPRESSIVE is thus a natural choice as a foundational printer, due to its expressiveness.
The code formatter currently supports only S-expression formatting. However, the task is already challenging. While the S-expression syntax may look simple and uniform, Racket users employ a variety of styles for different forms to make them look distinctive in order to improve readability. Each function application, for example, has three possible styles (while most languages have two function application styles). The search space of the code formatter is thus quite large.
The benchmarks (Table 3) consist of files of different sizes from the Racket language codebase14. class-internal and xform are the two largest files. We use the code formatter to format these files with the page width limit of 80. We run the code formatter twice, once with and once with .
1.9.3. 8.3 Results
Performance. The benchmarking results in Table 2 and Table 3 show that overall, PRETTYEXPRESSIVE is sufficiently fast in practice. While not the fastest, it can process large, practical workloads class-internal and xform under a second. Furthermore, it provides a performance guarantee even on tricky inputs. The same is not true for other printers. The Flatten benchmarks work very poorly for Wadler’s printer, and the FillSep benchmarks work very poorly for Bernardy’s printer. Interestingly, Bernardy’s naïve printer is faster than its practical variant, even though the latter is more optimized; this is due to the extra features that the practical printer needs to support. PRETTYEXPRESSIVE, by contrast, is set to support these features from the start.
We note two interesting observations of PRETTYEXPRESSIVE. First, it performs poorly on SExpFull relative to other printers. This is due to the memory pressure from memoization. Better engineering effort may be able to alleviate this issue. Second, although the time complexity of is , this worst case behavior happens only if Pareto frontiers are always full. In practice, this is not the case15, as evidenced by the fact that increasing tenfold does not multiply the running time by . On the contrary, increasing does not affect the running time at all on most benchmarks.
14https://github.com/racket/racket/tree/master/racket/collects at commit 4fla2bd4
15This observation also applies to Bernardy’s printers, which are also based on Pareto frontiers.
RandOver, and JSONW benchmarks. Upon manual inspection, we find that the layouts produced by PRETTYEXPRESSIVE are better. JSONW and RandOver are cases where there is an unavoidable overflow, causing Bernardy’s printer to overflow more than necessary. Figure 18 in Appendix A demonstrates this problem. RandFit and RandOver are cases where the greedy minimization and the align construct in Leijen’s printer interact poorly, as discussed in Bernardy [2017c]’s paper.
It should also be noted that neither Leijen’s nor Bernardy’s printers support custom optimality objectives, as their optimality objectives are integral to their algorithms. PRETTYEXPRESSIVE, by contrast, allows users to customize optimality objective via the cost factory.
Lastly, we evaluate the effectiveness of the tainting system. For almost every benchmark that has a tainted rendering () with the default , we find that using in an attempt to avoid taintedness16 yields the same result, confirming the optimality of the output layout. The only exception is the class-internal benchmark in Table 3, for which the output layouts are different in one line and otherwise identical, because the greedy heuristic in the taint operation prunes the optimal choice away. This demonstrates that despite being tainted, and thus no longer guaranteed to be optimal, the output layout is still reasonable (at least with respect to the cost factory that we employ and the heuristic to avoid bias described in Appendix C).
1.10. 9 CONCLUSION
We have described , an expressive printer that supports a variety of optimality objectives and is practically efficient. We developed a framework for reasoning about the expressiveness of PPLs, and we used this framework to guide the design of the PPL that targets. By surveying existing pretty printers, we have shown that is well-placed in the design space of printers. is proven correct in the Lean theorem prover and implemented as a practical printer PRETTYEXPRESSIVE, which powers a real-world code formatter for the Racket programming language. Our results show that PRETTYEXPRESSIVE (and ) is both pretty and fast.
1.11. DATA-AVAILABILITY STATEMENT
The latest version of the Racket PRETTYEXPRESSIVE17 and the Racket code formatter18 are available on GitHub. The main artifact, which consists of the above softwares and:
- • the Lean formalization (Sections 4 and 6)
- • the Rosette proofs (Section 3)
- • the OCaml PRETTYEXPRESSIVE (Section 7)
- • the benchmarks to reproduce our evaluation (Section 8)
is available on Docker,19 with its source on GitHub.20 A snapshot of the artifact is available on Zenodo [Porncharoenwase et al. 2023].
1.12. ACKNOWLEDGMENTS
We are thankful to the anonymous reviewers and the anonymous artifact reviewers for their very helpful feedback. This work is supported by the National Science Foundation under Grant Nos. CF-1651225, CCF-1836724, CNS-1844807, and by a gift from the VMware University Research Fund.
16Therefore, the Concat benchmarks do not count, since they are still tainted afterwards. The benchmarks are not interesting anyway, since there is no choice in the documents, so the output layouts are always optimal.
1.13. A AN ANALYSIS OF PRINTERS
group (text "AAA" <> nl) <>
nest 5 (group (text "B" <> nl <>
text "B" <> nl <> text "B")))
1 AAA 2 B B B
1 AAA 2 B 3 B
Fig. 16. A document in the traditional PPL and two of its corresponding layouts. Under the width limit of 5, the first layout is optimal—it does not overflow and occupies a minimal number of lines. By contrast, the second layout, which is produced by Wadler's printer, overflows and does not occupy a minimal number of lines.
let rec quadratic (n : int): doc = if n = 0 then text "line" else group (quadratic (n - 1) <> nl <> text "line")
Fig. 17. The function quadratic generates a document of size that Wadler's algorithm takes to print at any width limit, due to repeated flattening.
text "xxxxxxx" <<$> ((text "aaa" <<+> text "bbb") <<|> (text "aaa" <<$> text "bbb"))
1 xxxxxx 2 aaa 3 bbb
1 xxxxxx 2 aaabb
Fig. 18. A document in the arbitrary-choice PPL and two of its corresponding layouts. Under the width limit of 5, the first layout minimally overflows. By contrast, the second layout, which is produced by Bernardy's practical implementation, overflows more than necessary.
let rec mk (n : int): doc = if n = 0 then text "X" <<|> text "XX" else let subdoc = mk (n - 1) in (chr n <<+> subdoc <<+> chr n) <<|> subdoc
(a) The function mk generates a document whose DAG size is . denotes a text whose content is a string of length one that contains the th character.
(b) Let denote . Yelland's function would transform the original document into a restricted document where every aligned concatenation has a text as its left subdocument. However, the above derivation shows that the transformation has a combinatorial explosion. Define to be in Yelland's paper and to be . The derivation shows that is recursively transformed in different contexts.
Fig. 19. A family of documents that illustrates how the transformation in Yelland's algorithm does not necessarily preserve the sharing structure in the original document.
(* make an empty document of size n; n >= 1 *)
let rec make_dummy (n : int): doc =
if n = 1 then text ""
else text "" <-> make_dummy (n - 1)
(* make n lines; n >= 1 *)
let rec make_lines (n : int): doc =
if n = 1 then text ""
else text "" <-> make_lines (n - 1)
(* nth triangle number *)
let tri (n : int): int = n * (n + 1) / 2
let make_choices (k : int): doc =
let rec loop (i : int): doc =
let doc =
(make_lines i) <->
text (String.make (tri (k - i + 1)) 'a')
in if i = 1 then doc else doc <-> loop (i - 1)
in loop k
let rec example (k : int): doc =
let dummy = make_dummy (k * k) in
let giant = make_choices k in
dummy <-> giant
(a) The function example produces a document that triggers the worst-case time complexity of Yelland’s algorithm (that we are aware of). For a fixed , giant is a document with choices, where the -th choice has lines and characters ( is the triangle number function). Thus, its document tree size is . By concatenating giant with dummy, which is an “empty” document of size , the total document tree size is still . giant is designed so that it has segmented linear cost functions. Thus, the aligned concatenation of dummy and giant takes . By normalizing the document size to , we obtain that the time complexity of the printer is .
(b) A plot of the piecewise linear cost function (lines along the red dots) for giant in Figure 20a with . The x-axis is column positions at which giant will be printed. The y-axis is cost of giant. The plot consists of segmented cost functions, where each segment is a linear function. For simplicity, we assume that (1) the page width limit is 0; (2) there is no cost for newlines; and (3) the cost for each character past the page width limit is 1. Let be the -th choice in giant. The cost function for then is . These cost functions intersect at . Thus, the cost function for giant is unable to prune any segments away.
Fig. 20. In Yelland’s algorithm, every choiceless document (in the arbitrary-choice PPL) has an associated piecewise linear cost function , where determines the cost of ’s rendered layout at the column position . A general document similarly has an associated piecewise linear cost function , which takes the minimum of the cost functions from all choiceless documents that generates. The algorithm appears to be efficient at first glance, since taking the minimum can prune away many segmented linear cost functions. However, we are able to construct a document giant of size whose cost function has segmented linear cost functions, where is the tree size of the document. As the time complexity of the printer is where is the maximum number of piecewise linear cost functions in a cost function, we obtain .
1.14. B SELECTED PROOF SKETCHES
LEMMA 5.2. The arbitrary-choice PPL and are functionally complete.
PROOF SKETCH. For the arbitrary-choice PPL with the evaluation function , let be any non-empty set of layouts. For each where , we construct to be text s1i <-> ... <-> text s|li|}. Finally, we construct to be d1 <-> ... <-> d|L|}. We can see that . The proof for PPL is similar, but we replace a <-> b with a <-> nl <-> b.
LEMMA 5.3. The traditional PPL is not functionally complete.
PROOF SKETCH. It is not possible to construct a document in the traditional PPL that evaluates to the set of layouts . To see why, let be a function that joins all lines in a layout into a single line, with all whitespaces removed, and lift to work on a set of layouts (i.e., ). Let be the evaluation function for the traditional PPL. We can prove by induction that is a singleton set for any document . In other words, all layouts in are the same, modulo whitespaces. However, , which is not a singleton set. Hence, by congruence, no document can render to .
Note that there are other sets of layouts that are the same modulo whitespaces, but can't be evaluated to by the traditional PPL. An example is synchronized differences of spacing across multiple lines.
LEMMA 5.4. For each construct in , without is not functionally complete.
PROOF SKETCH. It is not possible to construct a document in each language in question that evaluates to the following set of layouts
without text. , because all we can produce is whitespaces.
without . , because all we can produce is at most two lines.
without nl. , because all we can produce is a single line.
without . , because all we can produce is a single layout.
THEOREM 5.12. Every construct in the traditional and arbitrary-choice PPLs is definable in .
PROOF SKETCH. The following syntactic abstractions can be used to define the constructs:
- • group is definable by
- • is definable by .
- • is definable by .
The rest of the constructs are already in .
THEOREM 5.16. Given a PPL and a construct , if there exists two documents and in and a relation such that , but , then is not definable in .
PROOF SKETCH. Let and denote the evaluation functions for and , respectively. We prove the contraposition. Assuming that is definable in , we need to prove that for any , and , implies . Let , and be arbitrary. We suppose that for all context in , holds, and need to prove that for all context in , holds.
Let be a context in . Because is definable in , we can perform a syntactic expansion on to obtain a context in such that for all document in . Hence, it suffices to prove that holds, but this is our hypothesis (instantiated with ).
THEOREM 5.17. The following is true:
- • is not definable in the arbitrary-choice PPL.
- • nest is not definable in the arbitrary-choice PPL.
- • group is not definable in the arbitrary-choice PPL.
- • is not definable in the traditional PPL.
PROOF SKETCH. In each proof, we need to show that is not definable in , where and are the construct and the PPL in question. We do so by providing a counterexample, which consists of documents and , and the relation . By induction, it can be shown that . We will further provide a counterexample context to show that . By Theorem 5.16, this suffices to show that is not definable in .
is not definable in the arbitrary-choice PPL. Given from Example 5.15, the counterexample is , , and . In particular, with , we have that , but .
nest is not definable in the arbitrary-choice PPL. Given from Example 5.15, the counterexample is , , and . In particular, with , we have that , but .
group is not definable in the arbitrary-choice PPL. Let be a function that finds the maximum number of the character "a" in lines of the layout, and lift to work on a set of layouts. The counterexample is , , and . In particular, with , we have that , but .
is not definable in the traditional PPL. Let be a function that counts the number of spaces in a layout (not counting newlines), and lift to work on a set of layouts. The counterexample is , , and . In particular, with , we have that , but .
LEMMA 5.18. If is not functionally complete, but is, then is not definable in .
PROOF SKETCH. Because is not functionally complete, there is a set of layouts that can't be evaluated to by any document in . Since is functionally complete, there is a document (which necessarily contains ) that evaluates to . Let and be any document in , and . Then holds trivially. However, with , we have that . This concludes the proof that is not definable in .
THEOREM 5.19. For any construct of , is not definable in .
PROOF SKETCH. The proofs for text, nl, , and are applications of Lemma 5.2, Lemma 5.4, and Lemma 5.18. The proofs for nest, flatten, and align are just like how we proved Theorem 5.17 for nest, group, and .
THEOREM 6.10. The time complexity of is where is the DAG size of the document.
PROOF SKETCH. The most expensive operation in the printer is concatenation (via CONCATRSSSET). The operation resolves the left sub-document, resulting in a measure set whose size is at most according to Lemma 6.8. It then resolves the right sub-document in at most different contexts. Thus, there are at most different measures from the right sub-document that the printer needs to concatenate and prune.
Consider . can range over different values. and can range over different values that are under . Hence, there are different contexts under the computation width limit. Multiplying this with the maximum units of computation in the previous paragraph, we obtain that the time complexity due to resolving within is , assuming that the resolver reuses memoized measure sets under the same context.
When is printed beyond , however, it can be fully resolved at most once, because:
- (1) While we would resolve both sub-documents of choice nodes, they would be all tainted, due to Lemma 6.9. Because all tainted measure sets are promises, all computations are delayed. The merge operation then chooses only one tainted measure set as the result, discarding the other one.
- (2) The document is properly shared, so under a given path, a document is encountered at most once.
As a result, the time complexity due to printing over is simply . Combining both parts, we obtain that the time complexity of is .
THEOREM 6.11. If a document is in the arbitrary-choice PPL, can print in .
PROOF SKETCH. In the arbitrary-choice PPL, is (mostly) maintained throughout the printing. Hence, there is one less dimension to consider, leading to the time complexity of .
1.15. C DISCUSSION
In this section, we broadly discuss the design of our work.
1.15.1. C.1 Additional Constructs
PRETTYEXPRESSIVE supports additional constructs fail, newline, and reset. The Racket PRETTYPEX- PRESSIVE further supports additional constructs full and cost. These constructs are out of scope for the paper, and we leave their formalization as future work.
Failure. fail widens to the empty set, thus introducing the possibility that a printing could fail. Furthermore, it is the identity for the operation . fail makes more expressive because it is impossible to make a document in evaluate to the empty set. In this sense, it could be said that is not truly “functionally complete,” but with fail is. Supporting fail can be done via rewriting rules: every document with fail can be normalized to a semantically-equivalent document without fail, or to a single fail. Hence, there is no need to modify the core printer to support the construct.
Generalized Newlines. newline is a straightforward generalization of nl so that flattening it can result in other possibilities besides a single space. When is Some , the flattened result is text . When is None, the flattened result is fail. With this construct:
- • nl is definable with newline (Some “_”).
- • break from Leijen [2000]’s printer is definable with newline (Some “”).
- • hard_nl is definable with newline None. With hard_nl, singleLine from Bernardy [2017b]’s practical printer is definable with flatten, given that the vertical concatenation uses hard_nl for entering a newline.
Reset. reset resets the indentation level to 0 for . This is useful for formatting multiline comments and here-string.
Fullness. full marks as full, which means there must be no more text after it in the same line. The construct is especially useful for formatting line comments, as it is illegal to put a piece of code after a line comment. A simpler variant of full is also implemented in Yelland’s printer for the R code formatter [Yelland 2015]. Unlike other extensions, which can be supported without significant changes to the core printer modification, full requires more involved changes.
- • The measure set definition is now required to recognize the empty set (where we prefer a tainted measure set over the empty set).
- • The resolver would consume two additional boolean arguments, which indicate the fullness status before and after the document.
- • Merging two tainted measure sets must now keep both tainted measure sets, and we may need to try both if the first one resolves to the empty set.
To keep the time complexity of the printer , we rely on the fact that “emptiness” in resolving (that is, resolving to the empty set) is independent from column positions and indentation levels. Thus, even though we now need to try many tainted measure sets, a document can be tried at most four times, which bounds the time complexity.
Cost. cost adds a cost to measures due to . This construct is not expressive in the traditional sense, as it does not affect layout results. However, it allows us to make weighted choices, so that we can prefer one style over another when all else is equal. Due to the flexibility of the cost factory, it is even possible to make multidimensional weights.
1.15.2. C.2 Safety
As shown in the proof of Lemma 5.3, the traditional PPL is not functionally complete because all layouts must have the same content, modulo whitespaces. While this property is restrictive for many tasks as elaborated in the paper, it does provide a sort of safety guarantee that the layouts will not be wildly different. <|>, however, allows us to violate this property. In fact, some arbitrary-choice printers (e.g. a prototype of Bernardy’s printer [Bernardy 2015]) intend that <|> should be restricted to maintain the property. Similarly, the inclusion of fail, newline, or full makes it possible to evaluate to an empty set, but the PPLs without the essence of fail provide a safety guarantee that an evaluation will never result in an empty set. Generally, the more expressive a language is, the more properties it will break, and the more burden will be put on the users to carefully use the constructs.
We argue that the spirit of these safety properties can still be accomplished in PPLs with a functionally complete core. One possible approach is similar to Wadler’s treatment of <|> and group: define high-level, “safe” constructs with just enough expressiveness to solve a domain-specific task, based on the core, “unsafe” constructs, and then hide these “unsafe” constructs away from the external interface. For example, one may hide <|>, and instead provide groupParen(d) = (text "(" <> d <> text ")") <|> flatten d, which evaluates to either with parentheses wrapped around, or the flattened . The language as defined by the external interface is no longer functionally complete, but enjoys the property that all layouts are the same modulo whitespaces and parentheses. Another possible approach is to export the core, “unsafe” constructs, but perform a static analysis to ensure that the document satisfies intended safety properties.
In any case, the expressive core constructs are what enable the advanced features that languages may require to be rendered well. Thus, our view is that an expressive printer is the key. We should start with an expressive albeit unsafe printer, rather than a safe but non-expressive one.
1.15.3. C.3 Memoization
While memoization is important to guarantee that will not take exponential time, it is also the performance bottleneck when the input document is large, due to too much memory allocation. In PRETTYEXPRESSIVE, we employ a heuristic to reduce memory allocation by adding a metadata memoization weight to each document node, which counts how long memoization has not been performed on descendant nodes. When the weight reaches a limit (set to 6 in our implementation), we perform memoization on the node, and reset the weight to 0. This can significantly speed up the performance of PRETTYEXPRESSIVE on some large documents.
1.15.4. C.4 Fusing Resolving and Rendering
One optimization in PRETTYEXPRESSIVE is to fuse together the resolving of a document to a measure set and the rendering of a choiceless document to a layout. This is done by replacing the doc component in a measure with a token function, which consumes a list of rendered tokens after the document is placed, and returns a new list of rendered tokens. A similar technique was employed by Podkopaev and Boulytchev [2015].
1.15.5. C.5 Handling Bias in the Presence of Taintedness
In Section 6, we see that the merge operation and thus the <|> operator is left-biased in the presence of taintedness. When exceeding is unavoidable, all text could be put in one line in the worst case if all left sub-documents use the “horizontal styling”! The proper solution is to increase . However, PRETTYEXPRESSIVE also implements a heuristic to infer a sub-document with the “vertical styling.” The heuristic adds a metadata that overestimates the number of lines for each document node. PRETTYEXPRESSIVE then uses a document with a larger overestimated number of lines as the left sub-document in choice documents.
1.15.6. C.6 Partial Evaluation
Similar to how we can perform partial evaluation in programming languages, we can also perform partial evaluation in PPL using rewriting rules. For example, a concatenation of two text can immediately be partially evaluated to a single text. However, this partial evaluation must be done with care to still preserve the sharing structure, since unconstrained rewriting may unfold the DAG structure into a tree, as illustrated in Figure 19. It is also worth noting that the partial evaluation may not necessarily preserve the semantics in the presence of taintedness. For example, one may want to reduce a nest n (text s) to text s for any and , but when , the document will definitely resolve to a tainted measure set, while the partially evaluated one does not necessarily.21
1.16. REFERENCES
- Pablo R Azero Alcocer and S Doaitse Swierstra. 1998. Optimal pretty-printing combinators. https://web.archive.org/web/20040911044443/http://www.cs.uu.nl/groups/ST/Software/PP/pabloicfp.ps.
- Jean-Philippe Bernardy. 2015. Towards The Prettiest Printer. https://jyp.github.io/posts/towards-the-prettyest-printer.html.
- Jean-Philippe Bernardy. 2017a. Disjunctionless. https://github.com/jyp/prettyest/pull/10.
- Jean-Philippe Bernardy. 2017b. prettyest. https://github.com/jyp/prettyest/blob/5e7a12cf37bb01467485bbe1e9d8f272fa4f8cd5/Text/PrettyPrint/Compact/Core.hs.
- Jean-Philippe Bernardy. 2017c. A Pretty but Not Greedy Printer (Functional Pearl). Proc. ACM Program. Lang. 1, ICFP, Article 6 (Aug. 2017), 21 pages. https://doi.org/10.1145/3110250
- Olaf Chitil. 2005. Pretty Printing with Lazy Dequeues. ACM Trans. Program. Lang. Syst. 27, 1 (jan 2005), 163–184. https://doi.org/10.1145/1053468.1053473
- Joëlle Coutaz. 1984. The box, a layout abstraction for user interface toolkits. (Dec. 1984). https://doi.org/10.1184/R1/6610382.v1
- Merijn De Jonge. 2002. Pretty-printing for software reengineering. In International Conference on Software Maintenance, 2002. Proceedings. IEEE, 550–559.
- Leonardo De Moura and Nikolaj Bjørner. 2008. Z3: An Efficient SMT Solver. In Proceedings of the 14th International Conference on Tools and Algorithms for the Construction and Analysis of Systems (TACAS). Budapest, Hungary, 337–340.
- ESLint. 2014. Change no-comma-dangle to comma-dangle. https://github.com/eslint/eslint/issues/1350.
- Matthias Felleisen. 1991. On the expressive power of programming languages. Science of Computer Programming 17, 1 (1991), 35–75. https://doi.org/10.1016/0167-6423(91)90036-W
21One may argue, however, that this semantic change is acceptable, because the change is for the better.
- Matthias Felleisen, Robert Bruce Findler, Matthew Flatt, Shriram Krishnamurthi, Eli Barzilay, Jay McCarthy, and Sam Tobin-Hochstadt. 2018. A Programmable Programming Language. Commun. ACM 61, 3 (March 2018), 62–71. https://doi.org/10.1145/3127323
- John Hughes. 1995. The design of a pretty-printing library. In Advanced Functional Programming, Johan Jeuring and Erik Meijer (Eds.). Springer Berlin Heidelberg, Berlin, Heidelberg, 53–96.
- Oleg Kiselyov, Simon Peyton-Jones, and Amr Sabry. 2012. Lazy v. Yield: Incremental, Linear Pretty-Printing. In Programming Languages and Systems, Ranjit Jhala and Atsushi Igarashi (Eds.). Springer Berlin Heidelberg, Berlin, Heidelberg, 190–206.
- Daan Leijen. 2000. wl-pprint: The Wadler/Leijen Pretty Printer. https://hackage.haskell.org/package/wl-pprint.
- Leonardo de Moura and Sebastian Ullrich. 2021. The Lean 4 Theorem Prover and Programming Language. In Automated Deduction – CADE 28, André Platzer and Geoff Suttcliffe (Eds.). Springer International Publishing, Cham, 625–635.
- Dereck C. Oppen. 1980. Prettyprinting. ACM Trans. Program. Lang. Syst. 2, 4 (Oct. 1980), 465–483. https://doi.org/10.1145/357114.357115
- Susan Owicki and David Gries. 1976. Verifying Properties of Parallel Programs: An Axiomatic Approach. Commun. ACM 19, 5 (May 1976), 279–285. https://doi.org/10.1145/360051.360224
- Simon Peyton-Jones. 1997. A pretty printer library in Haskell. https://web.archive.org/web/20080221052958/http://research.microsoft.com/Users/simonpj/downloads/pretty-printer/pretty.html. The identified mistakes are noted at https://github.com/haskell/pretty/blob/5b70d1be6e17a644dc3b5c80592cf7c5b339fd9/Text/PrettyPrint/HughesPJ.hs.
- Anton Podkopaev and Dmitri Boulytchev. 2015. Polynomial-Time Optimal Pretty-Printing Combinators with Choice. In Perspectives of System Informatics, Andrei Voronkov and Irina Virbitskaite (Eds.). Springer Berlin Heidelberg, Berlin, Heidelberg, 257–265.
- Sorawee Porncharoenwase, Luke Nelson, Xi Wang, and Emina Torlak. 2022. A Formal Foundation for Symbolic Evaluation with Merging. Proc. ACM Program. Lang. 6, POPL, Article 47 (Jan. 2022), 28 pages. https://doi.org/10.1145/3498709
- Sorawee Porncharoenwase, Justin Pombrio, and Emina Torlak. 2023. Artifact for A Pretty Expressive Printer. https://doi.org/10.5281/zenodo.8332960
- Prettier. 2016. Technical Details. https://prettier.io/docs/en/technical-details.html.
- S Doaitse Swierstra, Pablo R Azero Alcocer, and Joao Saraiva. 1999. Designing and Implementing Combinator Languages. In Third Summer School on Advanced Functional Programming, volume 1608 of LNCS. Springer-Verlag, 150–206.
- The Python Language Reference. 2010. Lexical analysis. https://docs.python.org/2.7/reference/lexical_analysis.html.
- Emina Torlak and Rastislav Bodik. 2014. A Lightweight Symbolic Virtual Machine for Solver-Aided Host Languages. In Proceedings of the 35th ACM SIGPLAN Conference on Programming Language Design and Implementation (PLDI). Edinburgh, United Kingdom, 530–541. https://doi.org/10.1145/2666356.2594340
- Philip Wadler. 2003. A prettier printer. The Fun of Programming, Cornerstones of Computing (2003), 223–243.
- Phillip Yelland. 2015. rfmt: A code formatter for R. https://github.com/google/rfmt.
- Phillip Yelland. 2016. A New Approach to Optimal Code Formatting. Technical note for open source project rfmt; https://github.com/google/rfmt.
Received 2023-04-14; accepted 2023-08-27