github.com/cockroachdb/cockroachdb-parser@v0.23.3-0.20240213214944-911057d40c9a/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  	"github.com/cockroachdb/cockroachdb-parser/pkg/util/errorutil"
    18  	"github.com/cockroachdb/errors"
    19  )
    20  
    21  // See the referenced paper in the package documentation for explanations
    22  // of the below code. Methods, variables, and implementation details were
    23  // made to resemble it as close as possible.
    24  
    25  // docBest represents a selected document as described by the type
    26  // "Doc" in the referenced paper (not "DOC"). This is the
    27  // less-abstract representation constructed during "best layout"
    28  // selection.
    29  type docBest struct {
    30  	tag docBestType
    31  	i   docPos
    32  	s   string
    33  	d   *docBest
    34  }
    35  
    36  type docBestType int
    37  
    38  const (
    39  	textB docBestType = iota
    40  	lineB
    41  	hardlineB
    42  	spacesB
    43  	keywordB
    44  )
    45  
    46  // Pretty returns a pretty-printed string for the Doc d at line length
    47  // n and tab width t. Keyword Docs are filtered through keywordTransform
    48  // if not nil. keywordTransform must not change the visible length of its
    49  // argument. It can, for example, add invisible characters like control codes
    50  // (colors, etc.).
    51  func Pretty(
    52  	d Doc, n int, useTabs bool, tabWidth int, keywordTransform func(string) string,
    53  ) (_ string, err error) {
    54  	defer func() {
    55  		if r := recover(); r != nil {
    56  			// This code allows us to propagate internal errors without having
    57  			// to add error checks everywhere throughout the code. This is only
    58  			// possible because the code does not update shared state and does
    59  			// not manipulate locks.
    60  			if ok, e := errorutil.ShouldCatch(r); ok {
    61  				err = e
    62  			} else {
    63  				// Other panic objects can't be considered "safe" and thus are
    64  				// propagated as panics.
    65  				panic(r)
    66  			}
    67  		}
    68  	}()
    69  
    70  	var sb strings.Builder
    71  	b := beExec{
    72  		w:                int16(n),
    73  		tabWidth:         int16(tabWidth),
    74  		memoBe:           make(map[beArgs]*docBest),
    75  		memoiDoc:         make(map[iDoc]*iDoc),
    76  		keywordTransform: keywordTransform,
    77  	}
    78  	ldoc := b.best(d)
    79  	b.layout(&sb, useTabs, ldoc)
    80  	return sb.String(), nil
    81  }
    82  
    83  // w is the max line width.
    84  func (b *beExec) best(x Doc) *docBest {
    85  	return b.be(docPos{0, 0}, b.iDoc(docPos{0, 0}, x, nil))
    86  }
    87  
    88  // iDoc represents the type [(Int,DOC)] in the paper,
    89  // extended with arbitrary string prefixes (not just int).
    90  // We'll use linked lists because this makes the
    91  // recursion more efficient than slices.
    92  type iDoc struct {
    93  	d    Doc
    94  	next *iDoc
    95  	i    docPos
    96  }
    97  
    98  type docPos struct {
    99  	tabs   int16
   100  	spaces int16
   101  }
   102  
   103  type beExec struct {
   104  	// w is the available line width.
   105  	w int16
   106  	// tabWidth is the virtual tab width.
   107  	tabWidth int16
   108  
   109  	// memoBe internalizes the results of the be function, so that the
   110  	// same value is not computed multiple times.
   111  	memoBe map[beArgs]*docBest
   112  
   113  	// memo internalizes iDoc objects to ensure they are unique in memory,
   114  	// and we can use pointer-pointer comparisons.
   115  	memoiDoc map[iDoc]*iDoc
   116  
   117  	// docAlloc speeds up the allocations of be()'s return values
   118  	// by (*beExec).newDocBest() defined below.
   119  	docAlloc []docBest
   120  
   121  	// idocAlloc speeds up the allocations by (*beExec).iDoc() defined
   122  	// below.
   123  	idocAlloc []iDoc
   124  
   125  	// keywordTransform filters keywords if not nil.
   126  	keywordTransform func(string) string
   127  
   128  	// beDepth is the depth of recursive calls of be. It is used to detect deep
   129  	// call stacks before a stack overflow occurs.
   130  	beDepth int
   131  }
   132  
   133  // maxBeDepth is the maximum allowed recursive call depth of be. If the depth
   134  // exceeds this value, be will panic.
   135  const maxBeDepth = 50_000
   136  
   137  // ErrPrettyMaxRecursionDepthExceeded is returned from Pretty when the maximum
   138  // recursion depth of function invoked by Pretty is exceeded.
   139  var ErrPrettyMaxRecursionDepthExceeded = errors.AssertionFailedf("max recursion depth exceeded")
   140  
   141  func (b *beExec) be(k docPos, xlist *iDoc) *docBest {
   142  	b.beDepth++
   143  	defer func() { b.beDepth-- }()
   144  	if b.beDepth > maxBeDepth {
   145  		panic(ErrPrettyMaxRecursionDepthExceeded)
   146  	}
   147  
   148  	// Shortcut: be k [] = Nil
   149  	if xlist == nil {
   150  		return nil
   151  	}
   152  
   153  	// If we've computed this result before, short cut here too.
   154  	memoKey := beArgs{k: k, d: xlist}
   155  	if cached, ok := b.memoBe[memoKey]; ok {
   156  		return cached
   157  	}
   158  
   159  	// General case.
   160  
   161  	d := *xlist
   162  	z := xlist.next
   163  
   164  	// Note: we'll need to memoize the result below.
   165  	var res *docBest
   166  
   167  	switch t := d.d.(type) {
   168  	case nilDoc:
   169  		res = b.be(k, z)
   170  	case *concat:
   171  		res = b.be(k, b.iDoc(d.i, t.a, b.iDoc(d.i, t.b, z)))
   172  	case nests:
   173  		res = b.be(k, b.iDoc(docPos{d.i.tabs, d.i.spaces + t.n}, t.d, z))
   174  	case nestt:
   175  		res = b.be(k, b.iDoc(docPos{d.i.tabs + 1 + d.i.spaces/b.tabWidth, 0}, t.d, z))
   176  	case text:
   177  		res = b.newDocBest(docBest{
   178  			tag: textB,
   179  			s:   string(t),
   180  			d:   b.be(docPos{k.tabs, k.spaces + int16(len(t))}, z),
   181  		})
   182  	case keyword:
   183  		res = b.newDocBest(docBest{
   184  			tag: keywordB,
   185  			s:   string(t),
   186  			d:   b.be(docPos{k.tabs, k.spaces + int16(len(t))}, z),
   187  		})
   188  	case line, softbreak:
   189  		res = b.newDocBest(docBest{
   190  			tag: lineB,
   191  			i:   d.i,
   192  			d:   b.be(d.i, z),
   193  		})
   194  	case hardline:
   195  		res = b.newDocBest(docBest{
   196  			tag: hardlineB,
   197  			i:   d.i,
   198  			d:   b.be(d.i, z),
   199  		})
   200  	case *union:
   201  		res = b.better(k,
   202  			b.be(k, b.iDoc(d.i, t.x, z)),
   203  			// We eta-lift the second argument to avoid eager evaluation.
   204  			func() *docBest {
   205  				return b.be(k, b.iDoc(d.i, t.y, z))
   206  			},
   207  		)
   208  	case *scolumn:
   209  		res = b.be(k, b.iDoc(d.i, t.f(k.spaces), z))
   210  	case *snesting:
   211  		res = b.be(k, b.iDoc(d.i, t.f(d.i.spaces), z))
   212  	case pad:
   213  		res = b.newDocBest(docBest{
   214  			tag: spacesB,
   215  			i:   docPos{spaces: t.n},
   216  			d:   b.be(docPos{k.tabs, k.spaces + t.n}, z),
   217  		})
   218  	default:
   219  		panic(fmt.Errorf("unknown type: %T", d.d))
   220  	}
   221  
   222  	// Memoize so we don't compute the same result twice.
   223  	b.memoBe[memoKey] = res
   224  
   225  	return res
   226  }
   227  
   228  // newDocBest makes a new docBest on the heap. Allocations
   229  // are batched for more efficiency.
   230  func (b *beExec) newDocBest(d docBest) *docBest {
   231  	buf := &b.docAlloc
   232  	if len(*buf) == 0 {
   233  		*buf = make([]docBest, 100)
   234  	}
   235  	r := &(*buf)[0]
   236  	*r = d
   237  	*buf = (*buf)[1:]
   238  	return r
   239  }
   240  
   241  // iDoc retrieves the unique instance of iDoc in memory for the given
   242  // values of i, s, d and z. The object is constructed if it does not
   243  // exist yet.
   244  //
   245  // The results of this function guarantee that the pointer addresses
   246  // are equal if the arguments used to construct the value were equal.
   247  func (b *beExec) iDoc(i docPos, d Doc, z *iDoc) *iDoc {
   248  	idoc := iDoc{i: i, d: d, next: z}
   249  	if m, ok := b.memoiDoc[idoc]; ok {
   250  		return m
   251  	}
   252  	r := b.newiDoc(idoc)
   253  	b.memoiDoc[idoc] = r
   254  	return r
   255  }
   256  
   257  // newiDoc makes a new iDoc on the heap. Allocations are batched
   258  // for more efficiency. Do not use this directly! Instead
   259  // use the iDoc() method defined above.
   260  func (b *beExec) newiDoc(d iDoc) *iDoc {
   261  	buf := &b.idocAlloc
   262  	if len(*buf) == 0 {
   263  		*buf = make([]iDoc, 100)
   264  	}
   265  	r := &(*buf)[0]
   266  	*r = d
   267  	*buf = (*buf)[1:]
   268  	return r
   269  }
   270  
   271  type beArgs struct {
   272  	d *iDoc
   273  	k docPos
   274  }
   275  
   276  func (b *beExec) better(k docPos, x *docBest, y func() *docBest) *docBest {
   277  	remainder := b.w - k.spaces - k.tabs*b.tabWidth
   278  	if fits(remainder, x) {
   279  		return x
   280  	}
   281  	return y()
   282  }
   283  
   284  func fits(w int16, x *docBest) bool {
   285  	if w < 0 {
   286  		return false
   287  	}
   288  	if x == nil {
   289  		// Nil doc.
   290  		return true
   291  	}
   292  	switch x.tag {
   293  	case textB, keywordB:
   294  		return fits(w-int16(len(x.s)), x.d)
   295  	case lineB:
   296  		return true
   297  	case hardlineB:
   298  		return false
   299  	case spacesB:
   300  		return fits(w-x.i.spaces, x.d)
   301  	default:
   302  		panic(fmt.Errorf("unknown type: %d", x.tag))
   303  	}
   304  }
   305  
   306  func (b *beExec) layout(sb *strings.Builder, useTabs bool, d *docBest) {
   307  	for ; d != nil; d = d.d {
   308  		switch d.tag {
   309  		case textB:
   310  			sb.WriteString(d.s)
   311  		case keywordB:
   312  			if b.keywordTransform != nil {
   313  				sb.WriteString(b.keywordTransform(d.s))
   314  			} else {
   315  				sb.WriteString(d.s)
   316  			}
   317  		case lineB, hardlineB:
   318  			sb.WriteByte('\n')
   319  			// Fill the tabs first.
   320  			padTabs := d.i.tabs * b.tabWidth
   321  			if useTabs {
   322  				for i := int16(0); i < d.i.tabs; i++ {
   323  					sb.WriteByte('\t')
   324  				}
   325  				padTabs = 0
   326  			}
   327  
   328  			// Fill the remaining spaces.
   329  			for i := int16(0); i < padTabs+d.i.spaces; i++ {
   330  				sb.WriteByte(' ')
   331  			}
   332  		case spacesB:
   333  			for i := int16(0); i < d.i.spaces; i++ {
   334  				sb.WriteByte(' ')
   335  			}
   336  		default:
   337  			panic(fmt.Errorf("unknown type: %d", d.tag))
   338  		}
   339  	}
   340  }