github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/util/pretty/pretty.go (about) 1 // Copyright 2018 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package pretty 12 13 import ( 14 "fmt" 15 "strings" 16 ) 17 18 // See the referenced paper in the package documentation for explanations 19 // of the below code. Methods, variables, and implementation details were 20 // made to resemble it as close as possible. 21 22 // docBest represents a selected document as described by the type 23 // "Doc" in the referenced paper (not "DOC"). This is the 24 // less-abstract representation constructed during "best layout" 25 // selection. 26 type docBest struct { 27 tag docBestType 28 i docPos 29 s string 30 d *docBest 31 } 32 33 type docBestType int 34 35 const ( 36 textB docBestType = iota 37 lineB 38 hardlineB 39 spacesB 40 keywordB 41 ) 42 43 // Pretty returns a pretty-printed string for the Doc d at line length 44 // n and tab width t. Keyword Docs are filtered through keywordTransform 45 // if not nil. keywordTransform must not change the visible length of its 46 // argument. It can, for example, add invisible characters like control codes 47 // (colors, etc.). 48 func Pretty(d Doc, n int, useTabs bool, tabWidth int, keywordTransform func(string) string) string { 49 var sb strings.Builder 50 b := beExec{ 51 w: int16(n), 52 tabWidth: int16(tabWidth), 53 memoBe: make(map[beArgs]*docBest), 54 memoiDoc: make(map[iDoc]*iDoc), 55 keywordTransform: keywordTransform, 56 } 57 ldoc := b.best(d) 58 b.layout(&sb, useTabs, ldoc) 59 return sb.String() 60 } 61 62 // w is the max line width. 63 func (b *beExec) best(x Doc) *docBest { 64 return b.be(docPos{0, 0}, b.iDoc(docPos{0, 0}, x, nil)) 65 } 66 67 // iDoc represents the type [(Int,DOC)] in the paper, 68 // extended with arbitrary string prefixes (not just int). 69 // We'll use linked lists because this makes the 70 // recursion more efficient than slices. 71 type iDoc struct { 72 d Doc 73 next *iDoc 74 i docPos 75 } 76 77 type docPos struct { 78 tabs int16 79 spaces int16 80 } 81 82 type beExec struct { 83 // w is the available line width. 84 w int16 85 // tabWidth is the virtual tab width. 86 tabWidth int16 87 88 // memoBe internalizes the results of the be function, so that the 89 // same value is not computed multiple times. 90 memoBe map[beArgs]*docBest 91 92 // memo internalizes iDoc objects to ensure they are unique in memory, 93 // and we can use pointer-pointer comparisons. 94 memoiDoc map[iDoc]*iDoc 95 96 // docAlloc speeds up the allocations of be()'s return values 97 // by (*beExec).newDocBest() defined below. 98 docAlloc []docBest 99 100 // idocAlloc speeds up the allocations by (*beExec).iDoc() defined 101 // below. 102 idocAlloc []iDoc 103 104 // keywordTransform filters keywords if not nil. 105 keywordTransform func(string) string 106 } 107 108 func (b *beExec) be(k docPos, xlist *iDoc) *docBest { 109 // Shortcut: be k [] = Nil 110 if xlist == nil { 111 return nil 112 } 113 114 // If we've computed this result before, short cut here too. 115 memoKey := beArgs{k: k, d: xlist} 116 if cached, ok := b.memoBe[memoKey]; ok { 117 return cached 118 } 119 120 // General case. 121 122 d := *xlist 123 z := xlist.next 124 125 // Note: we'll need to memoize the result below. 126 var res *docBest 127 128 switch t := d.d.(type) { 129 case nilDoc: 130 res = b.be(k, z) 131 case *concat: 132 res = b.be(k, b.iDoc(d.i, t.a, b.iDoc(d.i, t.b, z))) 133 case nests: 134 res = b.be(k, b.iDoc(docPos{d.i.tabs, d.i.spaces + t.n}, t.d, z)) 135 case nestt: 136 res = b.be(k, b.iDoc(docPos{d.i.tabs + 1 + d.i.spaces/b.tabWidth, 0}, t.d, z)) 137 case text: 138 res = b.newDocBest(docBest{ 139 tag: textB, 140 s: string(t), 141 d: b.be(docPos{k.tabs, k.spaces + int16(len(t))}, z), 142 }) 143 case keyword: 144 res = b.newDocBest(docBest{ 145 tag: keywordB, 146 s: string(t), 147 d: b.be(docPos{k.tabs, k.spaces + int16(len(t))}, z), 148 }) 149 case line, softbreak: 150 res = b.newDocBest(docBest{ 151 tag: lineB, 152 i: d.i, 153 d: b.be(d.i, z), 154 }) 155 case hardline: 156 res = b.newDocBest(docBest{ 157 tag: hardlineB, 158 i: d.i, 159 d: b.be(d.i, z), 160 }) 161 case *union: 162 res = b.better(k, 163 b.be(k, b.iDoc(d.i, t.x, z)), 164 // We eta-lift the second argument to avoid eager evaluation. 165 func() *docBest { 166 return b.be(k, b.iDoc(d.i, t.y, z)) 167 }, 168 ) 169 case *scolumn: 170 res = b.be(k, b.iDoc(d.i, t.f(k.spaces), z)) 171 case *snesting: 172 res = b.be(k, b.iDoc(d.i, t.f(d.i.spaces), z)) 173 case pad: 174 res = b.newDocBest(docBest{ 175 tag: spacesB, 176 i: docPos{spaces: t.n}, 177 d: b.be(docPos{k.tabs, k.spaces + t.n}, z), 178 }) 179 default: 180 panic(fmt.Errorf("unknown type: %T", d.d)) 181 } 182 183 // Memoize so we don't compute the same result twice. 184 b.memoBe[memoKey] = res 185 186 return res 187 } 188 189 // newDocBest makes a new docBest on the heap. Allocations 190 // are batched for more efficiency. 191 func (b *beExec) newDocBest(d docBest) *docBest { 192 buf := &b.docAlloc 193 if len(*buf) == 0 { 194 *buf = make([]docBest, 100) 195 } 196 r := &(*buf)[0] 197 *r = d 198 *buf = (*buf)[1:] 199 return r 200 } 201 202 // iDoc retrieves the unique instance of iDoc in memory for the given 203 // values of i, s, d and z. The object is constructed if it does not 204 // exist yet. 205 // 206 // The results of this function guarantee that the pointer addresses 207 // are equal if the arguments used to construct the value were equal. 208 func (b *beExec) iDoc(i docPos, d Doc, z *iDoc) *iDoc { 209 idoc := iDoc{i: i, d: d, next: z} 210 if m, ok := b.memoiDoc[idoc]; ok { 211 return m 212 } 213 r := b.newiDoc(idoc) 214 b.memoiDoc[idoc] = r 215 return r 216 } 217 218 // newiDoc makes a new iDoc on the heap. Allocations are batched 219 // for more efficiency. Do not use this directly! Instead 220 // use the iDoc() method defined above. 221 func (b *beExec) newiDoc(d iDoc) *iDoc { 222 buf := &b.idocAlloc 223 if len(*buf) == 0 { 224 *buf = make([]iDoc, 100) 225 } 226 r := &(*buf)[0] 227 *r = d 228 *buf = (*buf)[1:] 229 return r 230 } 231 232 type beArgs struct { 233 d *iDoc 234 k docPos 235 } 236 237 func (b *beExec) better(k docPos, x *docBest, y func() *docBest) *docBest { 238 remainder := b.w - k.spaces - k.tabs*b.tabWidth 239 if fits(remainder, x) { 240 return x 241 } 242 return y() 243 } 244 245 func fits(w int16, x *docBest) bool { 246 if w < 0 { 247 return false 248 } 249 if x == nil { 250 // Nil doc. 251 return true 252 } 253 switch x.tag { 254 case textB, keywordB: 255 return fits(w-int16(len(x.s)), x.d) 256 case lineB: 257 return true 258 case hardlineB: 259 return false 260 case spacesB: 261 return fits(w-x.i.spaces, x.d) 262 default: 263 panic(fmt.Errorf("unknown type: %d", x.tag)) 264 } 265 } 266 267 func (b *beExec) layout(sb *strings.Builder, useTabs bool, d *docBest) { 268 for ; d != nil; d = d.d { 269 switch d.tag { 270 case textB: 271 sb.WriteString(d.s) 272 case keywordB: 273 if b.keywordTransform != nil { 274 sb.WriteString(b.keywordTransform(d.s)) 275 } else { 276 sb.WriteString(d.s) 277 } 278 case lineB, hardlineB: 279 sb.WriteByte('\n') 280 // Fill the tabs first. 281 padTabs := d.i.tabs * b.tabWidth 282 if useTabs { 283 for i := int16(0); i < d.i.tabs; i++ { 284 sb.WriteByte('\t') 285 } 286 padTabs = 0 287 } 288 289 // Fill the remaining spaces. 290 for i := int16(0); i < padTabs+d.i.spaces; i++ { 291 sb.WriteByte(' ') 292 } 293 case spacesB: 294 for i := int16(0); i < d.i.spaces; i++ { 295 sb.WriteByte(' ') 296 } 297 default: 298 panic(fmt.Errorf("unknown type: %d", d.tag)) 299 } 300 } 301 }