diff --git a/doc/doc.go b/doc/doc.go index baa0d26..15af542 100644 --- a/doc/doc.go +++ b/doc/doc.go @@ -24,11 +24,31 @@ type Text string func (Text) isDoc() {} +// textNode is the heap or arena allocated form of Text: a pointer, so +// boxing it into Doc does not allocate. +type textNode struct { + s string +} + +func (textNode) isDoc() {} + +// NewText returns a doc for literal output. The value type Text also +// exists for direct construction; prefer NewText or Arena.Text so the +// node is a pointer. +func NewText(s string) Doc { return &textNode{s: s} } + // Concat is a sequence of documents printed in order. type Concat []Doc func (Concat) isDoc() {} +// concatNode is the arena allocated form of Concat. +type concatNode struct { + parts []Doc +} + +func (concatNode) isDoc() {} + // Join returns a Concat of parts joined by sep. func Join(sep Doc, parts []Doc) Doc { if len(parts) == 0 { @@ -47,6 +67,153 @@ func Join(sep Doc, parts []Doc) Doc { return out } +// Arena is a bump allocator for doc nodes. A document built through an +// arena allocates its nodes from a few growing regions instead of one +// heap allocation per node, at the cost of the arena retaining the +// regions until it is discarded. The arena must outlive the printing of +// the documents it built, and must not be used by concurrent builders. +// The zero value is ready to use. +type Arena struct { + texts []textNode + concats []concatNode + groups []group + ifs []ifBreak + indents []indent + aligns []align + suffixes []lineSuffix + parts []Doc + partsLen int +} + +// Reset reuses the arena's regions for a fresh document: the previous +// document built from this arena must be dead (no references to its +// nodes), as its storage is overwritten. The zero value is already +// reset. +func (a *Arena) Reset() { + a.partsLen = 0 + a.texts = a.texts[:0] + a.concats = a.concats[:0] + a.groups = a.groups[:0] + a.ifs = a.ifs[:0] + a.indents = a.indents[:0] + a.aligns = a.aligns[:0] + a.suffixes = a.suffixes[:0] +} + +// Text returns a doc for literal output, allocated from the arena. +func (a *Arena) Text(s string) Doc { + a.texts = append(a.texts, textNode{s: s}) + return &a.texts[len(a.texts)-1] +} + +// Concat returns a doc for parts, allocated from the arena. parts may +// be a Parts slice; it is kept as the node's children, so it must not +// be reused by the caller. +func (a *Arena) Concat(parts ...Doc) Doc { + a.concats = append(a.concats, concatNode{parts: parts}) + return &a.concats[len(a.concats)-1] +} + +// Join returns a doc for parts joined by sep, allocated from the arena. +func (a *Arena) Join(sep Doc, parts []Doc) Doc { + if len(parts) == 0 { + return a.Concat(nil) + } + + out := a.Parts(len(parts)*2 - 1) + for i, part := range parts { + if i > 0 { + out = append(out, sep) + } + + out = append(out, part) + } + + return a.Concat(out...) +} + +// Parts returns a scratch slice of docs with the given capacity, +// allocated from the arena's parts region. The region is never reused +// for the arena's lifetime, so the slice may become a node's children. +func (a *Arena) Parts(capacity int) []Doc { + if a.partsLen+capacity > len(a.parts) { + // Grow the single region geometrically. The old region stays + // reachable from slices handed out earlier, so it must not be + // reused; the copy is amortized. + n := len(a.parts) * 2 + if n < a.partsLen+capacity { + n = a.partsLen + capacity + } + + grown := make([]Doc, n) + copy(grown, a.parts[:a.partsLen]) + a.parts = grown + } + + s := a.parts[a.partsLen : a.partsLen+capacity : a.partsLen+capacity] + a.partsLen += capacity + + return s[:0] +} + +// Group wraps d in a group allocated from the arena. +func (a *Arena) Group(d Doc) Doc { + a.groups = append(a.groups, group{doc: d}) + return &a.groups[len(a.groups)-1] +} + +// GroupBreak wraps d in a group that always breaks, from the arena. +func (a *Arena) GroupBreak(d Doc) Doc { + a.groups = append(a.groups, group{doc: d, brk: true}) + return &a.groups[len(a.groups)-1] +} + +// GroupID wraps d in a group with an ID, from the arena. +func (a *Arena) GroupID(id int, d Doc) Doc { + a.groups = append(a.groups, group{doc: d, id: id}) + return &a.groups[len(a.groups)-1] +} + +// ConditionalGroup tries each state in order, from the arena. +func (a *Arena) ConditionalGroup(id int, states ...Doc) Doc { + a.groups = append(a.groups, group{doc: states[0], id: id, expanded: states}) + return &a.groups[len(a.groups)-1] +} + +// IfBreak builds an IfBreak for the innermost enclosing group, from the +// arena. +func (a *Arena) IfBreak(broken, flat Doc) Doc { + a.ifs = append(a.ifs, ifBreak{breakDoc: broken, flatDoc: flat}) + return &a.ifs[len(a.ifs)-1] +} + +// IfBreakFor builds an IfBreak following the group with the given ID, +// from the arena. +func (a *Arena) IfBreakFor(broken, flat Doc, groupID int) Doc { + a.ifs = append(a.ifs, ifBreak{breakDoc: broken, flatDoc: flat, groupID: groupID}) + return &a.ifs[len(a.ifs)-1] +} + +// Indent increases the indentation of its contents by one level, from +// the arena. +func (a *Arena) Indent(d Doc) Doc { + a.indents = append(a.indents, indent{doc: d}) + return &a.indents[len(a.indents)-1] +} + +// Align indents its contents by n columns, from the arena. +func (a *Arena) Align(n int, d Doc) Doc { + a.aligns = append(a.aligns, align{n: n, doc: d}) + return &a.aligns[len(a.aligns)-1] +} + +// LineSuffix prints its contents at the end of the current line, from +// the arena. +func (a *Arena) LineSuffix(d Doc) Doc { + a.suffixes = append(a.suffixes, lineSuffix{doc: d}) + return &a.suffixes[len(a.suffixes)-1] +} + // LineDoc is a line separator. In flat mode a plain line prints a space, a // soft line prints nothing, and a hard line prints a newline regardless of // mode. In break mode every line prints a newline followed by the current diff --git a/doc/dump.go b/doc/dump.go index 0ec909a..458f27d 100644 --- a/doc/dump.go +++ b/doc/dump.go @@ -26,6 +26,12 @@ func dumpDoc(b *strings.Builder, d Doc, ind string) { for _, c := range v { dumpDoc(b, c, ind+" ") } + case *concatNode: + fmt.Fprintf(b, "%sConcat\n", ind) + + for _, c := range v.parts { + dumpDoc(b, c, ind+" ") + } case *group: extra := "" if v.id != 0 { @@ -73,6 +79,12 @@ func dumpDoc(b *strings.Builder, d Doc, ind string) { } else { fmt.Fprintf(b, "%sText %q\n", ind, string(v)) } + case *textNode: + if v.s == "" { + fmt.Fprintf(b, "%sText \"\"\n", ind) + } else { + fmt.Fprintf(b, "%sText %q\n", ind, v.s) + } case LineDoc: kind := "Line" if v.Hard { diff --git a/doc/print.go b/doc/print.go index 77c0068..e3f4094 100644 --- a/doc/print.go +++ b/doc/print.go @@ -37,19 +37,18 @@ type command struct { doc Doc } -// indentation is the indentation of the current line: the string to emit and its -// display width. +// indentation is the indentation of the current line: the number of +// indent levels, any extra alignment columns, and the display width. +// The prefix string is materialized from the levels on write, so +// commands carry no string copies. type indentation struct { - value string + levels int + alignN int length int } func (i indentation) add(o Options) indentation { - if o.Indent == "" { - o.Indent = strings.Repeat(" ", o.TabWidth) - } - - i.value += o.Indent + i.levels++ i.length += o.TabWidth return i @@ -57,13 +56,13 @@ func (i indentation) add(o Options) indentation { func (i indentation) align(n int, o Options) indentation { if o.Indent != "\t" { - i.value += strings.Repeat(" ", n) + i.alignN += n i.length += n return i } // With tabs, a numeric alignment renders as one tab, matching Prettier. - i.value += "\t" + i.levels++ i.length += o.TabWidth return i @@ -110,6 +109,7 @@ type printer struct { // and would otherwise allocate a builder and a command stack per call. fitOutput strings.Builder fitCommands []command + prefixes []string // materialized indent prefixes, one per level } func (p *printer) write(s string) { @@ -151,6 +151,32 @@ func (p *printer) trim() int { return n } +// indentPrefix returns the materialized indentation string for levels +// indent levels, building each level once and caching it. +func (p *printer) indentPrefix(levels int) string { + if len(p.prefixes) == 0 { + p.prefixes = append(p.prefixes, "") + } + + for len(p.prefixes) <= levels { + p.prefixes = append(p.prefixes, p.prefixes[len(p.prefixes)-1]+p.o.Indent) + } + + return p.prefixes[levels] +} + +// writeIndent writes the indentation of a command: the indent-level +// prefix, then any extra alignment columns. +func (p *printer) writeIndent(i indentation) { + if i.levels > 0 { + p.write(p.indentPrefix(i.levels)) + } + + if i.alignN > 0 { + p.out = append(p.out, strings.Repeat(" ", i.alignN)...) + } +} + func (p *printer) run(d Doc) (string, error) { commands := []command{{indentation: indentation{}, mode: modeBreak, doc: d}} newLine := p.o.NewLine @@ -176,11 +202,27 @@ func (p *printer) run(d Doc) (string, error) { p.lastLineComment = false } + case *textNode: + if v.s != "" { + p.write(v.s) + + if len(commands) > 0 { + p.position += stringWidth(v.s) + } + + p.lastLineComment = false + } + case Concat: for _, v0 := range slices.Backward(v) { commands = append(commands, command{indentation: cmd.indentation, mode: cmd.mode, doc: v0}) } + case *concatNode: + for _, v0 := range slices.Backward(v.parts) { + commands = append(commands, command{indentation: cmd.indentation, mode: cmd.mode, doc: v0}) + } + case *indent: commands = append(commands, command{indentation: cmd.indentation.add(p.o), mode: cmd.mode, doc: v.doc}) @@ -279,14 +321,15 @@ func (p *printer) run(d Doc) (string, error) { // lines). if !v.Hard && p.lineEnded() && (!v.AfterComment || p.lastLineComment) { p.trim() - p.write(cmd.indentation.value) + p.writeIndent(cmd.indentation) p.position = cmd.indentation.length break } p.trim() - p.write(newLine + cmd.indentation.value) + p.write(newLine) + p.writeIndent(cmd.indentation) p.position = cmd.indentation.length // A comment's hard line marks the line as comment // ended; blank hard lines do not end a content line, @@ -376,6 +419,9 @@ func (p *printer) fits(next command, rest []command, remainingWidth int, hasLine p.fitCommands = p.fitCommands[:0] commands := append(p.fitCommands, next) + // Retain whatever capacity the loop grew to, so the next fits call + // starts from the larger backing array. + defer func() { p.fitCommands = commands[:0] }() for remainingWidth >= 0 { if p.fitsDBG { @@ -413,11 +459,29 @@ func (p *printer) fits(next command, rest []command, remainingWidth int, hasLine remainingWidth -= stringWidth(s) } + case *textNode: + if v.s != "" { + if hasPendingSpace { + output.WriteString(" ") + + remainingWidth-- + hasPendingSpace = false + } + + output.WriteString(v.s) + remainingWidth -= stringWidth(v.s) + } + case Concat: for _, v0 := range slices.Backward(v) { commands = append(commands, command{indentation: cmd.indentation, mode: cmd.mode, doc: v0}) } + case *concatNode: + for _, v0 := range slices.Backward(v.parts) { + commands = append(commands, command{indentation: cmd.indentation, mode: cmd.mode, doc: v0}) + } + case *indent, *align: commands = append(commands, command{indentation: cmd.indentation, mode: cmd.mode, doc: contentsOf(v)}) @@ -560,6 +624,10 @@ func traverseDoc(d Doc, enter func(Doc) bool, exit func(Doc), includeConditional for _, part := range v { traverseDoc(part, enter, exit, includeConditionalGroups) } + case *concatNode: + for _, part := range v.parts { + traverseDoc(part, enter, exit, includeConditionalGroups) + } case *group: if includeConditionalGroups { for _, state := range v.expanded { diff --git a/formatter/body.go b/formatter/body.go index f33c6bc..cfdec5c 100644 --- a/formatter/body.go +++ b/formatter/body.go @@ -19,7 +19,7 @@ func (f *formatter) structLike(v *syntax.Struct) doc.Doc { parts = append(parts, f.annotationsDoc(v.Annotations, v.Annotations != nil && v.Annotations.TokEnd() == v.TokEnd())) parts = append(parts, f.afterAnnotations(v.Annotations, v.TokEnd())) - return doc.Concat(parts) + return f.Concat(parts...) } // enum formats an enum declaration. @@ -34,7 +34,7 @@ func (f *formatter) enum(v *syntax.Enum) doc.Doc { parts = append(parts, f.annotationsDoc(v.Annotations, v.Annotations != nil && v.Annotations.TokEnd() == v.TokEnd())) parts = append(parts, f.afterAnnotations(v.Annotations, v.TokEnd())) - return doc.Concat(parts) + return f.Concat(parts...) } // scanKind returns the first token of the given kind in [start, end], or @@ -71,61 +71,61 @@ func (f *formatter) bracedBody(fields []*syntax.Field, open, close int, closeTra bodyID := f.id() sepMode := f.opts.Separator.Get(c) inner := append([]doc.Doc{doc.Line, f.fieldList(fields, bodyID, sepMode)}, f.ownLineComments(close)...) - closeBreak := doc.IfBreak(doc.SoftLine, doc.Text(" ")) + closeBreak := f.IfBreak(doc.SoftLine, f.Text(" ")) if len(fields) == 0 { inner = append([]doc.Doc{}, f.ownLineComments(close)...) - closeBreak = doc.IfBreak(doc.SoftLine, doc.Concat{}) + closeBreak = f.IfBreak(doc.SoftLine, f.Concat()) } openComments := f.sameLineComments(open) - openDoc := append([]doc.Doc{doc.Text(" {")}, openComments...) + openDoc := append([]doc.Doc{f.Text(" {")}, openComments...) if len(openComments) > 0 { openDoc = append(openDoc, doc.BreakParent) } - content := doc.Concat{ - doc.Concat(openDoc), - doc.Indent(doc.Concat(inner)), + content := f.Concat( + f.Concat(openDoc...), + f.Indent(f.Concat(inner...)), closeBreak, f.emitTokens(close, close, emitOpts{trailing: closeTrailing}), - } + ) if len(fields) > 0 && (f.opts.Break.Get(c) || sepForcesBreak(sepsOfFields(fields), sepMode)) { // BreakParent inside the group forces it to the broken layout. - content = doc.Concat{doc.BreakParent, content} + content = f.Concat(doc.BreakParent, content) } - return doc.GroupID(bodyID, content) + return f.GroupID(bodyID, content) } // bracedEnumBody is bracedBody for enum values. func (f *formatter) bracedEnumBody(values []*syntax.EnumValue, open, close int, closeTrailing bool) doc.Doc { bodyID := f.id() inner := append([]doc.Doc{doc.Line, f.enumValueList(values, bodyID)}, f.ownLineComments(close)...) - closeBreak := doc.IfBreak(doc.SoftLine, doc.Text(" ")) + closeBreak := f.IfBreak(doc.SoftLine, f.Text(" ")) if len(values) == 0 { inner = append([]doc.Doc{}, f.ownLineComments(close)...) - closeBreak = doc.IfBreak(doc.SoftLine, doc.Concat{}) + closeBreak = f.IfBreak(doc.SoftLine, f.Concat()) } openComments := f.sameLineComments(open) - openDoc := append([]doc.Doc{doc.Text(" {")}, openComments...) + openDoc := append([]doc.Doc{f.Text(" {")}, openComments...) if len(openComments) > 0 { openDoc = append(openDoc, doc.BreakParent) } - content := doc.Concat{ - doc.Concat(openDoc), - doc.Indent(doc.Concat(inner)), + content := f.Concat( + f.Concat(openDoc...), + f.Indent(f.Concat(inner...)), closeBreak, f.emitTokens(close, close, emitOpts{trailing: closeTrailing}), - } + ) if len(values) > 0 && (f.opts.Break.Get(ConstructEnum) || sepForcesBreak(sepsOfValues(values), f.opts.Separator.Get(ConstructEnum))) { - content = doc.Concat{doc.BreakParent, content} + content = f.Concat(doc.BreakParent, content) } - return doc.GroupID(bodyID, content) + return f.GroupID(bodyID, content) } // service formats a service declaration. The body is always multiline: @@ -134,7 +134,7 @@ func (f *formatter) service(v *syntax.Service) doc.Doc { open := f.scanKind(v.TokStart(), v.TokEnd(), syntax.TokenLBrace) close := f.scanKind(open+1, v.TokEnd(), syntax.TokenRBrace) - parts := make([]doc.Doc, 0, 8) + parts := f.Parts(8) for i, fn := range v.Functions { if i > 0 { @@ -149,20 +149,20 @@ func (f *formatter) service(v *syntax.Service) doc.Doc { parts = append(parts, f.function(fn)) } - inner := doc.Concat{doc.Concat(parts), doc.Concat(f.ownLineComments(close))} + inner := f.Concat(f.Concat(parts...), f.Concat(f.ownLineComments(close)...)) if len(v.Functions) > 0 { // The first function starts its own line; the closing trivia // provides the break before it for empty bodies, so the blank // count does not double. - inner = doc.Concat{doc.Line, doc.Concat(parts), doc.Concat(f.ownLineComments(close))} + inner = f.Concat(doc.Line, f.Concat(parts...), f.Concat(f.ownLineComments(close)...)) } else if !f.hasOwnLineComments(close) && f.token(close).BlankLinesBefore > 0 { // Empty body with blank lines before the close and no comments: // the blanks round-trip through the close's own line. - inner = doc.Concat{doc.Concat(f.blankLineDocs(f.token(close).BlankLinesBefore, doc.HardLine)), inner} + inner = f.Concat(f.Concat(f.blankLineDocs(f.token(close).BlankLinesBefore, doc.HardLine)...), inner) } openComments := f.sameLineComments(open) - openDoc := append([]doc.Doc{doc.Text(" {")}, openComments...) + openDoc := append([]doc.Doc{f.Text(" {")}, openComments...) if len(openComments) > 0 { openDoc = append(openDoc, doc.BreakParent) } @@ -170,12 +170,12 @@ func (f *formatter) service(v *syntax.Service) doc.Doc { // The line before the close collapses when the body ended with a line // comment (which owns its line end) and renders after a real blank // line, so the close always lands on its own line. - body := doc.Concat{ - doc.Concat(openDoc), - doc.Indent(inner), + body := f.Concat( + f.Concat(openDoc...), + f.Indent(inner), doc.AfterCommentLine, f.emitTokens(close, close, emitOpts{trailing: close != v.TokEnd()}), - } + ) out := []doc.Doc{ f.emitTokens(v.TokStart(), open, emitOpts{skipText: []int{open}}), @@ -183,7 +183,7 @@ func (f *formatter) service(v *syntax.Service) doc.Doc { } out = append(out, f.annotationsDoc(v.Annotations, v.Annotations != nil && v.Annotations.TokEnd() == v.TokEnd())) - return doc.Concat(out) + return f.Concat(out...) } // function formats a service method. The signature escalates via nested @@ -196,7 +196,7 @@ func (f *formatter) function(v *syntax.Function) doc.Doc { parts := append(f.ownLineComments(v.TokStart()), f.functionBody(v)) parts = append(parts, f.sameLineComments(v.TokEnd())...) - return doc.Concat(parts) + return f.Concat(parts...) } func (f *formatter) functionBody(v *syntax.Function) doc.Doc { @@ -219,19 +219,19 @@ func (f *formatter) functionBody(v *syntax.Function) doc.Doc { args := f.parenGroup(v.Args, open, f.parenClose(v.Args, open), false, argsMode) if v.Throws == nil { - return doc.Group(doc.Concat{ + return f.Group(f.Concat( header, args, f.functionTail(v, open), - }) + )) } - return doc.Group(doc.Concat{ + return f.Group(f.Concat( header, args, f.throwsGroup(v), f.functionTail(v, open), - }) + )) } // parenGroup renders "(fields)" as its own group, folding independently: @@ -241,15 +241,15 @@ func (f *formatter) functionBody(v *syntax.Function) doc.Doc { func (f *formatter) parenGroup(fields []*syntax.Field, open, close int, forced bool, sepMode SeparatorMode) doc.Doc { broken := f.brokenParens(fields, open, close, sepMode) if forced { - broken = doc.Concat{broken, doc.BreakParent} + broken = f.Concat(broken, doc.BreakParent) } - flat := append([]doc.Doc{doc.Text("(")}, f.sameLineComments(open)...) - flat = append(flat, f.flatFieldsJoin(fields, sepMode), doc.Text(")")) + flat := append([]doc.Doc{f.Text("(")}, f.sameLineComments(open)...) + flat = append(flat, f.flatFieldsJoin(fields, sepMode), f.Text(")")) - return doc.Group(doc.IfBreak( + return f.Group(f.IfBreak( broken, - doc.Concat(flat), + f.Concat(flat...), )) } @@ -258,7 +258,7 @@ func (f *formatter) parenGroup(fields []*syntax.Field, open, close int, forced b func (f *formatter) throwsGroup(v *syntax.Function) doc.Doc { forced := f.fieldsForcedBroken(v.Throws.Fields) || sepForcesBreak(sepsOfFields(v.Throws.Fields), f.opts.Separator.Get(ConstructThrows)) || f.opts.Break.Get(ConstructThrows) - return doc.Concat{doc.Text(" throws "), f.parenGroup(v.Throws.Fields, v.Throws.TokStart(), v.Throws.TokEnd(), forced, f.opts.Separator.Get(ConstructThrows))} + return f.Concat(f.Text(" throws "), f.parenGroup(v.Throws.Fields, v.Throws.TokStart(), v.Throws.TokEnd(), forced, f.opts.Separator.Get(ConstructThrows))) } // parenClose returns the close paren index matching the open paren at @@ -316,19 +316,19 @@ func sepsOfValues(values []*syntax.EnumValue) []syntax.TokenKind { // field's own separator when preserving, or a single forced separator per // the sepMode. func (f *formatter) flatFieldsJoin(fields []*syntax.Field, sepMode SeparatorMode) doc.Doc { - parts := make([]doc.Doc, 0, len(fields)) + parts := f.Parts(len(fields)) for i, field := range fields { if i > 0 { - parts = append(parts, doc.Concat{ - doc.Text(sepText(fields[i-1].Sep, sepMode)), + parts = append(parts, f.Concat( + f.Text(sepText(fields[i-1].Sep, sepMode)), doc.Line, - }) + )) } parts = append(parts, f.fieldContent(field, nil, false, sepMode)) } - return doc.Concat(parts) + return f.Concat(parts...) } // functionBrokenArgs renders the signature with arguments and throws both @@ -346,7 +346,7 @@ func (f *formatter) functionBrokenArgs(v *syntax.Function, header doc.Doc) doc.D parts = append(parts, f.functionTail(v, open)) - return doc.Concat(parts) + return f.Concat(parts...) } // functionTail renders the tokens of the function after its args and @@ -354,7 +354,7 @@ func (f *formatter) functionBrokenArgs(v *syntax.Function, header doc.Doc) doc.D // annotations, and any stray tokens lenient sources leave — everything the // structural layout does not emit itself. open is the args open paren. func (f *formatter) functionTail(v *syntax.Function, open int) doc.Doc { - parts := make([]doc.Doc, 0, 8) + parts := f.Parts(8) argsClose := f.parenClose(v.Args, open) if argsClose < v.TokEnd() { @@ -385,14 +385,14 @@ func (f *formatter) functionTail(v *syntax.Function, open int) doc.Doc { parts = append(parts, f.emitTokens(f.nextReal(idx), v.TokEnd(), emitOpts{leading: true})) } - return doc.Concat(parts) + return f.Concat(parts...) } // brokenFields renders fields one per line, each with its trailing // separator per the sepMode option. Comments and blank lines inside // the list are preserved. func (f *formatter) brokenFields(fields []*syntax.Field, sepMode SeparatorMode) doc.Doc { - parts := make([]doc.Doc, 0, 8) + parts := f.Parts(8) for i, field := range fields { if i > 0 { @@ -407,7 +407,7 @@ func (f *formatter) brokenFields(fields []*syntax.Field, sepMode SeparatorMode) parts = append(parts, f.fieldDoc(field, f.alignmentFor(fields, i, sepMode), 0, sepMode)) } - return doc.Concat(parts) + return f.Concat(parts...) } // brokenParens renders "open, fields, close" one field per line, or just @@ -415,18 +415,18 @@ func (f *formatter) brokenFields(fields []*syntax.Field, sepMode SeparatorMode) // indices; their comments are preserved even with no fields. func (f *formatter) brokenParens(fields []*syntax.Field, open, close int, sepMode SeparatorMode) doc.Doc { if len(fields) == 0 { - return doc.Concat{ - doc.Text("("), - doc.Concat(f.sameLineComments(open)), + return f.Concat( + f.Text("("), + f.Concat(f.sameLineComments(open)...), f.emitTokens(close, close, emitOpts{leading: true}), - } + ) } inner := append([]doc.Doc{doc.Line, f.brokenFields(fields, sepMode)}, f.ownLineComments(close)...) - parts := append([]doc.Doc{doc.Text("(")}, f.sameLineComments(open)...) - parts = append(parts, doc.Indent(doc.Concat(inner)), doc.Line, doc.Text(")")) + parts := append([]doc.Doc{f.Text("(")}, f.sameLineComments(open)...) + parts = append(parts, f.Indent(f.Concat(inner...)), doc.Line, f.Text(")")) - return doc.Concat(parts) + return f.Concat(parts...) } // fieldsForcedBroken reports whether any field has comments or blank lines @@ -474,7 +474,7 @@ func (f *formatter) blankLines(n syntax.Node, line doc.Doc) []doc.Doc { // blankLineDocs returns count copies of line, for blank lines that must be // preserved exactly. func (f *formatter) blankLineDocs(count int, line doc.Doc) []doc.Doc { - parts := make([]doc.Doc, 0, count) + parts := f.Parts(count) for range count { parts = append(parts, line) } diff --git a/formatter/comments.go b/formatter/comments.go index 78cd037..0e42236 100644 --- a/formatter/comments.go +++ b/formatter/comments.go @@ -44,13 +44,13 @@ func lineComment(k syntax.TokenKind) bool { func (f *formatter) ownLineComment(c, prev int, first bool) []doc.Doc { ct := f.token(c) - parts := make([]doc.Doc, 0, 4) + parts := f.Parts(4) if prev >= 0 || !first || ct.BlankLinesBefore > 0 { parts = append(parts, doc.SoftLine) } parts = append(parts, f.blankLineDocs(ct.BlankLinesBefore, doc.HardLine)...) - parts = append(parts, doc.Text(trimComment(ct.Text)), doc.CommentLine) + parts = append(parts, f.Text(trimComment(ct.Text)), doc.CommentLine) return parts } @@ -59,8 +59,9 @@ func (f *formatter) ownLineComment(c, prev int, first bool) []doc.Doc { // real token's line: a space, the comment text, and — for line comments — // a hard line owning its line end. It reports whether the comment ended // the line. -func sameLineComment(ct syntax.Token) ([]doc.Doc, bool) { - parts := []doc.Doc{doc.Text(" "), doc.Text(trimComment(ct.Text))} +func (f *formatter) sameLineComment(ct syntax.Token) ([]doc.Doc, bool) { + parts := f.Parts(2) + parts = append(parts, f.Text(" "), f.Text(trimComment(ct.Text))) if lineComment(ct.Kind) { return append(parts, doc.CommentLine), true } @@ -90,7 +91,7 @@ func (f *formatter) sameLineRun(idx int, fn func(c int)) { func (f *formatter) ownLineComments(idx int) []doc.Doc { prev := f.prevReal(idx - 1) - parts := make([]doc.Doc, 0, 4) + parts := f.Parts(4) first := true for c := prev + 1; c < idx; c++ { @@ -113,10 +114,10 @@ func (f *formatter) ownLineComments(idx int) []doc.Doc { // sameLineComments renders the same-line comments after the real token at // idx. func (f *formatter) sameLineComments(idx int) []doc.Doc { - parts := make([]doc.Doc, 0, 4) + parts := f.Parts(4) f.sameLineRun(idx, func(c int) { - docs, _ := sameLineComment(f.token(c)) + docs, _ := f.sameLineComment(f.token(c)) parts = append(parts, docs...) }) @@ -130,14 +131,14 @@ func (f *formatter) sameLineComments(idx int) []doc.Doc { // the run ended the line, in which case the caller must not emit the // canonical gap. func (f *formatter) commentsRun(prev, cur int) ([]doc.Doc, bool) { - parts := make([]doc.Doc, 0, 4) + parts := f.Parts(4) lineEnded := false for c := prev + 1; c < cur; c++ { ct := f.token(c) if ct.Line == f.token(prev).Line { // Same-line: inline after the previous token. - docs, ended := sameLineComment(ct) + docs, ended := f.sameLineComment(ct) parts = append(parts, docs...) lineEnded = ended @@ -156,7 +157,7 @@ func (f *formatter) commentsRun(prev, cur int) ([]doc.Doc, bool) { // separator (the mode drops its text) that cannot share the previous // content's line: each starts its own line, so the output round-trips. func (f *formatter) suppressedSepComments(idx int) []doc.Doc { - parts := make([]doc.Doc, 0, 4) + parts := f.Parts(4) f.sameLineRun(idx, func(c int) { parts = append(parts, f.ownLineComment(c, idx, false)...) diff --git a/formatter/field.go b/formatter/field.go index 86cbbe0..595e243 100644 --- a/formatter/field.go +++ b/formatter/field.go @@ -14,11 +14,11 @@ import ( // the body to break. Column alignment applies per blank-line group, // matching the previous formatter. func (f *formatter) fieldList(fields []*syntax.Field, bodyID int, sepMode SeparatorMode) doc.Doc { - parts := make([]doc.Doc, 0, 8) + parts := f.Parts(8) for i, field := range fields { if i > 0 { - parts = append(parts, fieldSep(fields[i-1].Sep, sepMode)) + parts = append(parts, f.fieldSep(fields[i-1].Sep, sepMode)) parts = append(parts, f.blankLines(field, doc.HardLine)...) } else { parts = append(parts, f.blankLines(field, doc.HardLine)...) @@ -34,8 +34,8 @@ func (f *formatter) fieldList(fields []*syntax.Field, bodyID int, sepMode Separa // enclosing group breaks, otherwise the separator text — per-field when // preserving (each item keeps its own trailing separator), or a single // forced separator per the comma mode. -func fieldSep(prevSep syntax.TokenKind, mode SeparatorMode) doc.Doc { - return doc.IfBreak(doc.Line, doc.Concat{doc.Text(sepText(prevSep, mode)), doc.Line}) +func (f *formatter) fieldSep(prevSep syntax.TokenKind, mode SeparatorMode) doc.Doc { + return f.IfBreak(doc.Line, f.Concat(f.Text(sepText(prevSep, mode)), doc.Line)) } // sepText is the separator text between two flat list items: each item's @@ -63,11 +63,11 @@ func sepText(prevSep syntax.TokenKind, mode SeparatorMode) string { // enumValueList formats enum bodies with the same layout as fieldList. func (f *formatter) enumValueList(values []*syntax.EnumValue, bodyID int) doc.Doc { - parts := make([]doc.Doc, 0, 8) + parts := f.Parts(8) for i, value := range values { if i > 0 { - parts = append(parts, fieldSep(values[i-1].Sep, f.opts.Separator.Get(ConstructEnum))) + parts = append(parts, f.fieldSep(values[i-1].Sep, f.opts.Separator.Get(ConstructEnum))) parts = append(parts, f.blankLines(value, doc.HardLine)...) } else { parts = append(parts, f.blankLines(value, doc.HardLine)...) @@ -320,13 +320,13 @@ func (f *formatter) nodeTrailingInline(end int, sep syntax.TokenKind, sepMode Se func (f *formatter) fieldDoc(v *syntax.Field, align *columnAlign, bodyID int, sepMode SeparatorMode) doc.Doc { content := f.fieldContent(v, align, false, sepMode) if bodyID != 0 { - content = doc.IfBreakFor( - doc.Concat{f.fieldContent(v, align, true, sepMode), trailingSep(v.Sep, sepMode)}, + content = f.IfBreakFor( + f.Concat(f.fieldContent(v, align, true, sepMode), f.trailingSep(v.Sep, sepMode)), content, bodyID, ) } else { - content = doc.Concat{f.fieldContent(v, align, true, sepMode), trailingSep(v.Sep, sepMode)} + content = f.Concat(f.fieldContent(v, align, true, sepMode), f.trailingSep(v.Sep, sepMode)) } parts := append(f.ownLineComments(v.TokStart()), content) @@ -444,8 +444,8 @@ func (f *formatter) fieldPads(v *syntax.Field, a *columnAlign) ([]padEntry, stri func (f *formatter) enumValue(v *syntax.EnumValue, align *columnAlign, bodyID int) doc.Doc { content := f.enumValueContent(v, align, false, f.opts.Separator.Get(ConstructEnum)) if bodyID != 0 { - content = doc.IfBreakFor( - doc.Concat{f.enumValueContent(v, align, true, f.opts.Separator.Get(ConstructEnum)), trailingSep(v.Sep, f.opts.Separator.Get(ConstructEnum))}, + content = f.IfBreakFor( + f.Concat(f.enumValueContent(v, align, true, f.opts.Separator.Get(ConstructEnum)), f.trailingSep(v.Sep, f.opts.Separator.Get(ConstructEnum))), content, bodyID, ) @@ -519,23 +519,23 @@ func sepEmits(sep syntax.TokenKind, mode SeparatorMode) bool { // trailingSep returns the trailing separator for the given original // separator, per the comma mode: always a comma or semicolon when forcing, // nothing when removing, the original separator when preserving. -func trailingSep(sep syntax.TokenKind, mode SeparatorMode) doc.Doc { +func (f *formatter) trailingSep(sep syntax.TokenKind, mode SeparatorMode) doc.Doc { switch mode { case SeparatorComma: - return doc.Text(",") + return f.Text(",") case SeparatorSemicolon: - return doc.Text(";") + return f.Text(";") case SeparatorNone: - return doc.Concat{} + return f.Concat() default: switch sep { case syntax.TokenComma: - return doc.Text(",") + return f.Text(",") case syntax.TokenSemicolon: - return doc.Text(";") + return f.Text(";") } - return doc.Concat{} + return f.Concat() } } diff --git a/formatter/format.go b/formatter/format.go index d9320c4..7ec1ade 100644 --- a/formatter/format.go +++ b/formatter/format.go @@ -12,6 +12,7 @@ import ( "errors" "fmt" "strings" + "sync" "github.com/karitham/thrift-ls/doc" "github.com/karitham/thrift-ls/syntax" @@ -208,6 +209,17 @@ func (o Options) normalize() Options { // Format renders a parsed document. The document must have been parsed // without errors; callers check the parse errors before formatting. Format // is deterministic and pure: it reads nothing but the document and options. +// formatArena is the shared node arena for Format and FormatNode. The +// doc IR dies with the print, so a single pooled arena is safe: regions +// are reused across calls and the garbage the GC would scan shrinks to +// the region overflows. +var ( + formatArena doc.Arena + formatMu sync.Mutex +) + +// Format renders the whole document. The arena is pooled: the returned +// string is the only thing that outlives the call. func Format(d *syntax.Document, o Options) (string, error) { if d == nil { return "", errors.New("formatter: nil document") @@ -215,17 +227,41 @@ func Format(d *syntax.Document, o Options) (string, error) { o = o.normalize() - return PrintIR(BuildIR(d, o), o) + formatMu.Lock() + defer formatMu.Unlock() + + formatArena.Reset() + + f := &formatter{ + Arena: &formatArena, + doc: d, + toks: d.Tokens, + opts: o, + } + + return doc.Print(f.document(), printOptions(o)) } -// BuildIR builds the document IR for the given options. The IR can be -// inspected with doc.Dump before printing; the printer mutates groups in -// place, so dump after PrintIR to see the layout decisions. +// printOptions maps formatter options to printer options. +func printOptions(o Options) doc.Options { + return doc.Options{ + PrintWidth: o.PrintWidth, + Indent: o.Indent, + TabWidth: o.TabWidth, + NewLine: "\n", + } +} + +// BuildIR builds the document IR for the given options, from a fresh +// arena: the IR outlives the call and can be inspected with doc.Dump +// before printing; the printer mutates groups in place, so dump after +// PrintIR to see the layout decisions. func BuildIR(d *syntax.Document, o Options) doc.Doc { f := &formatter{ - doc: d, - toks: d.Tokens, - opts: o, + Arena: &doc.Arena{}, + doc: d, + toks: d.Tokens, + opts: o, } return f.document() @@ -233,12 +269,7 @@ func BuildIR(d *syntax.Document, o Options) doc.Doc { // PrintIR prints the document IR. func PrintIR(ir doc.Doc, o Options) (string, error) { - return doc.Print(ir, doc.Options{ - PrintWidth: o.PrintWidth, - Indent: o.Indent, - TabWidth: o.TabWidth, - NewLine: "\n", - }) + return doc.Print(ir, printOptions(o)) } // FormatNode renders a single node with its comments, for hover previews @@ -250,22 +281,23 @@ func FormatNode(d *syntax.Document, n syntax.Node, o Options) (string, error) { o = o.normalize() + formatMu.Lock() + defer formatMu.Unlock() + + formatArena.Reset() + f := &formatter{ - doc: d, - toks: d.Tokens, - opts: o, + Arena: &formatArena, + doc: d, + toks: d.Tokens, + opts: o, } - ir := f.node(n) - return doc.Print(ir, doc.Options{ - PrintWidth: o.PrintWidth, - Indent: o.Indent, - TabWidth: o.TabWidth, - NewLine: "\n", - }) + return doc.Print(f.node(n), printOptions(o)) } type formatter struct { + *doc.Arena doc *syntax.Document toks []syntax.Token opts Options @@ -357,9 +389,9 @@ func (f *formatter) nextReal(idx int) int { // structural layout emits itself (text replaces the suppressed text when // set); pads widen alignment columns. func (f *formatter) emitTokens(start, end int, o emitOpts) doc.Doc { - parts := make([]doc.Doc, 0, 8) + parts := f.Parts(8) if o.prefix != "" { - parts = append(parts, doc.Text(o.prefix)) + parts = append(parts, f.Text(o.prefix)) } if o.leading { @@ -395,14 +427,14 @@ func (f *formatter) emitTokens(start, end int, o emitOpts) doc.Doc { text = "oneway" } - parts = append(parts, doc.Text(text)) + parts = append(parts, f.Text(text)) } else if o.text != "" { - parts = append(parts, doc.Text(o.text)) + parts = append(parts, f.Text(o.text)) } if !skipped && o.pads != nil { if pad := padAt(o.pads, i); pad != "" { - parts = append(parts, doc.Text(pad)) + parts = append(parts, f.Text(pad)) } } @@ -429,7 +461,7 @@ func (f *formatter) emitTokens(start, end int, o emitOpts) doc.Doc { } } - return doc.Concat(parts) + return f.Concat(parts...) } // tokenGap returns the canonical gap between two adjacent real tokens: @@ -437,16 +469,16 @@ func (f *formatter) emitTokens(start, end int, o emitOpts) doc.Doc { // line, the canonical spacing otherwise. func (f *formatter) tokenGap(prev, cur int) doc.Doc { if f.sameLineEndsLine(prev) { - return doc.Concat{} + return f.Concat() } - return doc.Text(rawTokenGap(f.token(prev), f.token(cur))) + return f.Text(rawTokenGap(f.token(prev), f.token(cur))) } // foldBreak is the foldable gap after an opening token or separating // comma: a line in the broken layout, a space (or nothing) flat. func (f *formatter) foldBreak(i int, flat string) doc.Doc { - return doc.IfBreak(doc.Line, doc.Text(flat)) + return f.IfBreak(doc.Line, f.Text(flat)) } // commaSep renders a separating comma with its trivia, then the foldable @@ -498,7 +530,7 @@ func (f *formatter) node(n syntax.Node) doc.Doc { parts := append(f.ownLineComments(n.TokStart()), f.nodeBody(n)) parts = append(parts, f.sameLineComments(n.TokEnd())...) - return doc.Concat(parts) + return f.Concat(parts...) } // nodeBody formats a node without its comments. @@ -528,7 +560,7 @@ func (f *formatter) nodeBody(n syntax.Node) doc.Doc { // document assembles the whole file: top-level nodes separated by // collapsible lines, blank lines preserved, and trailing comments. func (f *formatter) document() doc.Doc { - parts := make([]doc.Doc, 0, 8) + parts := f.Parts(8) for i, n := range f.doc.Nodes { if i > 0 { @@ -550,7 +582,7 @@ func (f *formatter) document() doc.Doc { parts = append(parts, doc.Line) } - return doc.Concat(parts) + return f.Concat(parts...) } // --- headers --------------------------------------------------------------- @@ -578,7 +610,7 @@ func (f *formatter) namespace(v *syntax.Namespace) doc.Doc { parts = append(parts, f.annotationsDoc(v.Annotations, v.Annotations != nil && v.Annotations.TokEnd() == v.TokEnd())) parts = append(parts, f.afterAnnotations(v.Annotations, v.TokEnd())) - return doc.Concat(parts) + return f.Concat(parts...) } func (f *formatter) typedef(v *syntax.Typedef) doc.Doc { @@ -596,7 +628,7 @@ func (f *formatter) typedef(v *syntax.Typedef) doc.Doc { parts = append(parts, f.annotationsDoc(v.Annotations, v.Annotations != nil && v.Annotations.TokEnd() == v.TokEnd())) parts = append(parts, f.afterAnnotations(v.Annotations, v.TokEnd())) - return doc.Concat(parts) + return f.Concat(parts...) } func (f *formatter) constant(v *syntax.Const) doc.Doc { @@ -623,7 +655,7 @@ func (f *formatter) constant(v *syntax.Const) doc.Doc { parts = append(parts, f.emitTokens(f.nextReal(value.TokEnd()+1), v.TokEnd(), emitOpts{leading: true})) } - return doc.Concat(parts) + return f.Concat(parts...) } // --- annotations ----------------------------------------------------------- @@ -633,7 +665,7 @@ func (f *formatter) constant(v *syntax.Const) doc.Doc { // their comments. func (f *formatter) afterAnnotations(a *syntax.Annotations, end int) doc.Doc { if a == nil || a.TokEnd() >= end { - return doc.Concat{} + return f.Concat() } return f.emitTokens(f.nextReal(a.TokEnd()+1), end, emitOpts{leading: true}) @@ -644,27 +676,27 @@ func (f *formatter) afterAnnotations(a *syntax.Annotations, end int) doc.Doc { // commas render as token runs, so comments inside the parens are preserved. func (f *formatter) annotationsDoc(a *syntax.Annotations, isLast bool) doc.Doc { if a == nil { - return doc.Concat{} + return f.Concat() } open, close := a.TokStart(), a.TokEnd() if len(a.Items) == 0 { - out := doc.Concat{ - doc.Text(" "), + out := f.Concat( + f.Text(" "), f.emitTokens(open, open, emitOpts{leading: true, trailing: true}), f.emitTokens(close, close, emitOpts{leading: true}), - } + ) if !isLast { // Same-line comments after the close render at the group // boundary, outside it. - out = doc.Concat{out, doc.Concat(f.sameLineComments(close))} + out = f.Concat(out, f.Concat(f.sameLineComments(close)...)) } return out } all := emitOpts{leading: true, trailing: true} - middle := make([]doc.Doc, 0, len(a.Items)*2) + middle := f.Parts(len(a.Items) * 2) last := open for i, item := range a.Items { @@ -694,18 +726,18 @@ func (f *formatter) annotationsDoc(a *syntax.Annotations, isLast bool) doc.Doc { last = lastItem.TokEnd() } - group := doc.Group(doc.Concat{ + group := f.Group(f.Concat( f.emitTokens(open, open, all), - doc.Indent(doc.Concat{f.foldBreak(open, ""), doc.Concat(middle)}), + f.Indent(f.Concat(f.foldBreak(open, ""), f.Concat(middle...))), f.foldBreak(last, ""), f.emitTokens(close, close, emitOpts{leading: true}), - }) + )) - out := doc.Concat{doc.Text(" "), group} + out := f.Concat(f.Text(" "), group) if !isLast { // Same-line comments after the close render at the group boundary, // outside the group, so the group folds independently. - out = doc.Concat{out, doc.Concat(f.sameLineComments(close))} + out = f.Concat(out, f.Concat(f.sameLineComments(close)...)) } return out diff --git a/formatter/value.go b/formatter/value.go index 4551f54..68237ff 100644 --- a/formatter/value.go +++ b/formatter/value.go @@ -19,7 +19,7 @@ func isListSep(kind syntax.TokenKind) bool { // belongs to the declaration's trailing comments. func (f *formatter) constValue(v *syntax.ConstValue, isLast bool) doc.Doc { if v == nil { - return doc.Concat{} + return f.Concat() } switch v.Kind { @@ -78,13 +78,13 @@ func (f *formatter) constItems(items []constItem, open, close int, c Construct, openOpts := emitOpts{trailing: true} if len(items) == 0 { - return doc.Concat{ + return f.Concat( f.emitTokens(open, open, openOpts), f.emitTokens(close, close, emitOpts{leading: true}), - } + ) } - middle := make([]doc.Doc, 0, len(items)*2) + middle := f.Parts(len(items) * 2) for i, item := range items { if i > 0 { @@ -116,21 +116,21 @@ func (f *formatter) constItems(items []constItem, open, close int, c Construct, // The single break before the close. A line comment owns its line end // (HardLine); the printer collapses a following structural line, so // the SoftLine never leaves a blank line. - closeBreak := doc.IfBreak(doc.SoftLine, doc.Text("")) + closeBreak := f.IfBreak(doc.SoftLine, f.Text("")) - inner := doc.Concat{ + inner := f.Concat( f.emitTokens(open, open, openOpts), - doc.Indent(doc.Concat{f.foldBreak(open, ""), doc.Concat(middle)}), + f.Indent(f.Concat(f.foldBreak(open, ""), f.Concat(middle...))), trailing, closeBreak, f.emitTokens(close, close, closeOpts), - } + ) if f.opts.Break.Get(c) || sepForcesBreakList(f.sepsOf(items), sepMode) { // BreakParent inside the group forces it to the broken layout. - inner = doc.Concat{doc.BreakParent, inner} + inner = f.Concat(doc.BreakParent, inner) } - return doc.Group(inner) + return f.Group(inner) } // sepForcesBreakList reports whether a preserved separator mix forces the @@ -206,7 +206,7 @@ func (f *formatter) trailingItemSep(last int, mode SeparatorMode) doc.Doc { } if !hasSep && text == "" { - return doc.Concat{} + return f.Concat() } if hasSep && text == f.token(sep).Text { @@ -215,7 +215,7 @@ func (f *formatter) trailingItemSep(last int, mode SeparatorMode) doc.Doc { // line end instead. sepDoc := f.emitTokens(sep, sep, emitOpts{leading: true, trailing: true}) if !f.sameLineEndsLine(sep) { - sepDoc = doc.Concat{sepDoc, doc.Text(" ")} + sepDoc = f.Concat(sepDoc, f.Text(" ")) } return sepDoc @@ -224,7 +224,7 @@ func (f *formatter) trailingItemSep(last int, mode SeparatorMode) doc.Doc { if !hasSep { // No source separator: the forced text and the flat gap before // the closing bracket. - return doc.Concat{doc.Text(text), doc.Text(" ")} + return f.Concat(f.Text(text), f.Text(" ")) } // Forced text (or dropping the source separator under SeparatorNone): @@ -234,7 +234,7 @@ func (f *formatter) trailingItemSep(last int, mode SeparatorMode) doc.Doc { // is stable across a reparse. sepDoc := f.emitTokens(sep, sep, emitOpts{leading: true, trailing: true, skipText: []int{sep}, text: text}) if text != "" && !f.sameLineEndsLine(sep) { - sepDoc = doc.Concat{sepDoc, doc.Text(" ")} + sepDoc = f.Concat(sepDoc, f.Text(" ")) } return sepDoc