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  }