@@ -3,91 +3,81 @@ import {
33 leftOperandNeedsParenthesis ,
44 rightOperandNeedsParenthesis ,
55} from "../../../formulas/parser" ;
6+ import { memoize } from "../../../helpers" ;
67
78/**
8- * This file implements a prettifier for Abstract Syntax Trees (ASTs) used in
9- * spreadsheet formulas. The prettifier converts an AST into a human-readable
10- * string representation, ensuring proper formatting with indentation, line
11- * breaks, and grouping based on available width constraints.
9+ * Pretty-prints formula ASTs into readable formulas.
1210 *
13- * The core functionality revolves around the `Doc` structure, which represents
14- * formatting elements such as concatenation, nesting, and line insertion. The
15- * prettifier dynamically selects the best formatting option based on the
16- * available width, producing a compact or expanded representation as needed .
11+ * Implements a Wadler-inspired pretty printer:
12+ * it converts an AST into a `Doc` structure,
13+ * and then chooses between compact (flat) or expanded (with
14+ * line breaks and indentation) layouts depending on space .
1715 *
18- * The implementation is inspired by the blog article:
19- * https://lik.ai/blog/how-a-pretty-printer-works/
20- *
21- * Key components:
22- * - `Doc` structure: Represents formatting options for ASTs.
23- * - `print`: Converts a `Doc` into a string representation that fits within a
24- * specified width.
25- * - `astToDoc`: Transforms an AST into a `Doc` structure for formatting.
26- * - Dynamic line breaking and indentation: Ensures the output is readable and
27- * fits within constraints.
16+ * References:
17+ * - https://lik.ai/blog/how-a-pretty-printer-works/
18+ * - Wadler, "A prettier printer": https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf
2819 */
29-
30- export function prettify ( ast : AST ) {
20+ export function prettify ( ast : AST ) : string {
3121 return "=" + print ( astToDoc ( ast ) , 59 ) ; // 59 but 60 with the `=` at the beginning
3222}
3323
3424// ---------------------------------------
3525// Doc structure
3626// ---------------------------------------
3727
38- // The `Doc` structure represents the possible ways to format an Abstract Syntax
39- // Tree (AST) into a human-readable string.
40- // It includes formatting elements such as line breaks, indentation, and token
41- // grouping.
42- //
43- // Once created, this structure is used to determine the best formatting path
44- // based on the available width, allowing dynamic selection of the most suitable
45- // string representation for the given constraints.
46-
28+ /**
29+ * A `Doc` represents alternative layouts (tree of layouts) for pretty-printing.
30+ * The printer chooses the layout that fits best within the available width.
31+ */
4732type Doc = string | ChooseBetween | Concat | Nest | InsertLine ;
4833
4934type ChooseBetween = { type : "chooseBetween" ; doc1 : Doc ; doc2 : Doc } ;
5035type Concat = { type : "concat" ; docs : Doc [ ] } ;
5136type Nest = { type : "nest" ; indentLevel : number ; doc : Doc } ;
5237type InsertLine = { type : "insertLine" } ;
5338
54- /** Represents a line break that can be inserted when formatting the document.
55- * Used in groups to dynamically decide where to break lines based on available space.
39+ /**
40+ * A possible line break.
41+ * Printed as either space or newline.
5642 */
5743function line ( ) : InsertLine {
5844 return { type : "insertLine" } ;
5945}
6046
61- /** Represents a nested structure with a specific indentation level.
62- * Useful for formatting hierarchical or indented content .
47+ /**
48+ * Increase indentation for a nested block .
6349 */
6450function nest ( indentLevel : number , doc : Doc ) : Nest {
6551 return { type : "nest" , indentLevel, doc } ;
6652}
6753
68- /** Concatenates multiple `Doc` elements into a single `Doc`.
69- * This is the primary way to combine strings and other formatting elements.
54+ /**
55+ * Combines multiple docs into a single doc, concatenating them side by side
56+ * without any line breaks or indentation.
7057 */
7158function concat ( docs : Doc [ ] ) : Concat {
7259 return { type : "concat" , docs } ;
7360}
7461
75- /** Groups a `Doc` to allow dynamic selection between a flattened version
76- * (single-line representation) and the original structure based on available space.
62+ /**
63+ * Marks a document as a unit to be printed "flat" (all on one line)
64+ * if it fits within the width, otherwise with line breaks.
7765 */
7866function group ( doc : Doc ) : ChooseBetween {
7967 return chooseBetween ( flatten ( doc ) , doc ) ;
8068}
8169
82- /** Creates a choice between two `Doc` structures, typically used in `group`.
83- * The choice depends on whether the available space can accommodate the flattened version.
70+ /**
71+ * Creates a choice between two alternative layouts.
72+ * The formatter tries `doc1`; if it does not fit within
73+ * the line width, it falls back to `doc2`.
8474 */
8575function chooseBetween ( doc1 : Doc , doc2 : Doc ) : ChooseBetween {
8676 return { type : "chooseBetween" , doc1, doc2 } ;
8777}
8878
89- /** Flattens a `Doc` structure into a single-line representation.
90- * Used exclusively for `group` to create the flattened alternative .
79+ /**
80+ * Flattens a doc into its single-line form .
9181 */
9282function flatten ( doc : Doc ) : Doc {
9383 if ( typeof doc === "string" ) {
@@ -120,34 +110,27 @@ function flatten(doc: Doc): Doc {
120110
121111/**
122112 * A linked list for string segments.
113+ * Used to avoid large string concatenations during layout selection.
123114 */
124115interface LinkedString {
125116 subString : string ;
126117 next : LinkedString | null ;
127118}
128119
129- /**
130- * Memoized helper to build indentation strings.
131- */
132- const memoizedIndentation : Record < number , string > = { } ;
133- function getIndentation ( indentLevel : number ) : string {
134- if ( ! ( indentLevel in memoizedIndentation ) ) {
135- memoizedIndentation [ indentLevel ] = "\n" + "\t" . repeat ( indentLevel ) ;
136- }
137- return memoizedIndentation [ indentLevel ] ;
138- }
120+ const getIndentationString = memoize ( function getIndentationString ( indentLevel : number ) : string {
121+ return "\n" + "\t" . repeat ( indentLevel ) ;
122+ } ) ;
139123
140124/**
141- * Converts a `Doc` structure into a string representation that fits within
125+ * Converts a `Doc` into a string representation that fits within
142126 * the specified width.
143127 */
144128function print ( doc : Doc , width : number ) : string {
145- return stringify ( selectBestLinkedString ( width , doc ) ) ;
129+ return stringify ( selectBestLayout ( width , doc ) ) ;
146130}
147131
148132/**
149- * Recursively converts a `LinkedString` object into a human-readable string.
150- * This is the final step of the prettifier process.
133+ * Join all segments of a LinkedString into the final string.
151134 */
152135function stringify ( linkedString : LinkedString | null ) : string {
153136 let result = "" ;
@@ -159,16 +142,15 @@ function stringify(linkedString: LinkedString | null): string {
159142}
160143
161144/**
162- * Selects the best `LinkedString` representation of a `Doc` that fits within
163- * the given width.
145+ * Layout selection for a `Doc` that fits within the given width.
164146 */
165- function selectBestLinkedString ( width : number , doc : Doc ) : LinkedString | null {
147+ function selectBestLayout ( width : number , doc : Doc ) : LinkedString | null {
166148 const head : RestToFitNode = {
167149 indentLevel : 0 ,
168150 doc,
169151 next : null ,
170152 } ;
171- return _selectBestLinkedString ( width , 0 , head ) ;
153+ return _selectBestLayout ( width , 0 , head ) ;
172154}
173155
174156/**
@@ -180,7 +162,7 @@ interface RestToFitNode {
180162 next : RestToFitNode | null ;
181163}
182164
183- function _selectBestLinkedString (
165+ function _selectBestLayout (
184166 width : number ,
185167 currentIndentLevel : number ,
186168 head : RestToFitNode | null
@@ -194,44 +176,44 @@ function _selectBestLinkedString(
194176 if ( typeof doc === "string" ) {
195177 return {
196178 subString : doc ,
197- next : _selectBestLinkedString ( width , currentIndentLevel + doc . length , next ) ,
179+ next : _selectBestLayout ( width , currentIndentLevel + doc . length , next ) ,
198180 } ;
199181 }
200182 if ( doc . type === "concat" ) {
201183 let newHead = next ;
202184 for ( let i = doc . docs . length - 1 ; i >= 0 ; i -- ) {
203185 newHead = { indentLevel, doc : doc . docs [ i ] , next : newHead } ;
204186 }
205- return _selectBestLinkedString ( width , currentIndentLevel , newHead ) ;
187+ return _selectBestLayout ( width , currentIndentLevel , newHead ) ;
206188 }
207189 if ( doc . type === "nest" ) {
208- return _selectBestLinkedString ( width , currentIndentLevel , {
190+ return _selectBestLayout ( width , currentIndentLevel , {
209191 indentLevel : indentLevel + doc . indentLevel ,
210192 doc : doc . doc ,
211193 next,
212194 } ) ;
213195 }
214196 if ( doc . type === "insertLine" ) {
215197 return {
216- subString : getIndentation ( indentLevel ) ,
217- next : _selectBestLinkedString ( width , indentLevel , next ) ,
198+ subString : getIndentationString ( indentLevel ) ,
199+ next : _selectBestLayout ( width , indentLevel , next ) ,
218200 } ;
219201 }
220202 if ( doc . type === "chooseBetween" ) {
221203 const head1 = { indentLevel, doc : doc . doc1 , next } ;
222- const possibleLinkedString = _selectBestLinkedString ( width , currentIndentLevel , head1 ) ;
204+ const possibleLinkedString = _selectBestLayout ( width , currentIndentLevel , head1 ) ;
223205 if ( fits ( width - currentIndentLevel , possibleLinkedString ) ) {
224206 return possibleLinkedString ;
225207 }
226208
227209 const head2 = { indentLevel, doc : doc . doc2 , next } ;
228- return _selectBestLinkedString ( width , currentIndentLevel , head2 ) ;
210+ return _selectBestLayout ( width , currentIndentLevel , head2 ) ;
229211 }
230212 return null ;
231213}
232214
233215/**
234- * Checks whether a given `LinkedString` fits within the specified width.
216+ * Check if a layout fits on a single line within width.
235217 */
236218function fits ( width : number , linkedString : LinkedString | null ) : boolean {
237219 while ( linkedString ) {
@@ -245,13 +227,6 @@ function fits(width: number, linkedString: LinkedString | null): boolean {
245227 return true ;
246228}
247229
248- // ---------------------------------------
249- // AST part
250- // ---------------------------------------
251-
252- /**
253- * Converts an AST into a `Doc` structure for formatting.
254- */
255230function astToDoc ( ast : AST ) : Doc {
256231 switch ( ast . type ) {
257232 case "NUMBER" :
@@ -268,7 +243,7 @@ function astToDoc(ast: AST): Doc {
268243
269244 case "FUNCALL" :
270245 const docs = ast . args . map ( astToDoc ) ;
271- return splitParenthesesContent (
246+ return wrapInParentheses (
272247 concat ( docs . map ( ( doc , i ) => ( i < 1 ? doc : concat ( [ ", " , line ( ) , doc ] ) ) ) ) ,
273248 ast . value
274249 ) ;
@@ -278,7 +253,7 @@ function astToDoc(ast: AST): Doc {
278253 const needParenthesis = ast . postfix
279254 ? leftOperandNeedsParenthesis ( ast )
280255 : rightOperandNeedsParenthesis ( ast ) ;
281- const finalOperandDoc = needParenthesis ? splitParenthesesContent ( operandDoc ) : operandDoc ;
256+ const finalOperandDoc = needParenthesis ? wrapInParentheses ( operandDoc ) : operandDoc ;
282257
283258 return ast . postfix
284259 ? concat ( [ finalOperandDoc , ast . value ] )
@@ -287,11 +262,11 @@ function astToDoc(ast: AST): Doc {
287262 case "BIN_OPERATION" : {
288263 const leftDoc = astToDoc ( ast . left ) ;
289264 const needParenthesisLeftDoc = leftOperandNeedsParenthesis ( ast ) ;
290- const finalLeftDoc = needParenthesisLeftDoc ? splitParenthesesContent ( leftDoc ) : leftDoc ;
265+ const finalLeftDoc = needParenthesisLeftDoc ? wrapInParentheses ( leftDoc ) : leftDoc ;
291266
292267 const rightDoc = astToDoc ( ast . right ) ;
293268 const needParenthesisRightDoc = rightOperandNeedsParenthesis ( ast ) ;
294- const finalRightDoc = needParenthesisRightDoc ? splitParenthesesContent ( rightDoc ) : rightDoc ;
269+ const finalRightDoc = needParenthesisRightDoc ? wrapInParentheses ( rightDoc ) : rightDoc ;
295270
296271 const operator = `${ ast . value } ` ;
297272 return group ( concat ( [ finalLeftDoc , operator , nest ( 1 , concat ( [ line ( ) , finalRightDoc ] ) ) ] ) ) ;
@@ -306,9 +281,9 @@ function astToDoc(ast: AST): Doc {
306281}
307282
308283/**
309- * Wraps a `Doc` in parentheses and optionally prepends a function name.
284+ * Wraps a `Doc` in parentheses (with optional function name) .
310285 */
311- function splitParenthesesContent ( doc : Doc , functionName : undefined | string = undefined ) : Doc {
286+ function wrapInParentheses ( doc : Doc , functionName : undefined | string = undefined ) : Doc {
312287 const docToConcat = [ "(" , nest ( 1 , concat ( [ line ( ) , doc ] ) ) , line ( ) , ")" ] ;
313288 if ( functionName ) {
314289 docToConcat . unshift ( functionName ) ;
0 commit comments