Skip to content

Commit 6253f92

Browse files
committed
Implement separate infrastructure for formatting and printing output
This fixes various issues around code folding and comments and reduces duplicated logic for dealing with both.
1 parent 456501e commit 6253f92

File tree

11 files changed

+512
-326
lines changed

11 files changed

+512
-326
lines changed

.github/workflows/go.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ jobs:
3030
go-version: '^1.18.0'
3131
- name: go fmt
3232
run: |
33-
go fmt | tee -a modified
34-
go fmt ./cmd/protoscope | tee -a modified
33+
go fmt . ./cmd/* ./internal/* | tee -a modified
3534
if [[ $(cat modified) ]]; then exit 1; fi;
3635
3736
tests:

internal/print/print.go

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://wwp.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// package print contains printing helpers used by the Protoscope disassembler.
16+
17+
package print
18+
19+
import (
20+
"bytes"
21+
"fmt"
22+
"unicode/utf8"
23+
)
24+
25+
// Stack is a wrapper over a slice type that provides helpers for pushing and
26+
// popping elements.
27+
//
28+
// Exported because of utility in the disassembler itself.
29+
type Stack[T any] []T
30+
31+
// Push pushes an element.
32+
func (s *Stack[T]) Push(x T) {
33+
*s = append(*s, x)
34+
}
35+
36+
// Pop pops an element; panics if the stack is empty.
37+
func (s *Stack[T]) Pop() T {
38+
popped := (*s)[len(*s)-1]
39+
*s = (*s)[:len(*s)-1]
40+
return popped
41+
}
42+
43+
// Pop pops the top n elements off the stack and returns a slice containing
44+
// copies. Panics if the stack is too small
45+
func (s *Stack[T]) PopN(n int) []T {
46+
popped := make([]T, n)
47+
copy(popped, (*s)[len(*s)-n:])
48+
*s = (*s)[:len(*s)-n]
49+
return popped
50+
}
51+
52+
// Peek returns a pointer to the top of the stack, or nil if the stack is
53+
// empty.
54+
func (s *Stack[T]) Peek() *T {
55+
return &s.PeekN(1)[0]
56+
}
57+
58+
// Peek returns the top n elements of the stack as another stack.
59+
//
60+
// Returns nil if the stack is too small.
61+
func (s *Stack[T]) PeekN(n int) Stack[T] {
62+
if len(*s) < n {
63+
return nil
64+
}
65+
return (*s)[len(*s)-n:]
66+
}
67+
68+
// Line represents a single line in the output stream. A Printer buffers on a
69+
// line-by-line basis to be able to do indentation and brace collapse with
70+
// minimal difficulty.
71+
type Line struct {
72+
// This line's in-progress text buffer.
73+
bytes.Buffer
74+
75+
remarks []string
76+
indent int
77+
folds int
78+
}
79+
80+
// Printer is an intelligent indentation and codeblock aware printer.
81+
type Printer struct {
82+
// The number of spaces to use per indentation level.
83+
Indent int
84+
// The number of nested folded blocks allowed, < 0 means infinity.
85+
MaxFolds int
86+
87+
lines Stack[Line]
88+
blocks Stack[BlockInfo]
89+
}
90+
91+
// Current returns the current line being processed.
92+
func (p *Printer) Current() *Line {
93+
return p.Prev(0)
94+
}
95+
96+
// Discards the current line
97+
func (p *Printer) DiscardLine() {
98+
p.lines.Pop()
99+
}
100+
101+
type Mark int
102+
103+
// Makes a mark on the line buffer.
104+
func (p *Printer) Mark() Mark {
105+
return Mark(len(p.lines))
106+
}
107+
108+
// Discards all lines after the mark.
109+
func (p *Printer) Reset(m Mark) {
110+
p.lines = p.lines[:m]
111+
}
112+
113+
// Prev returns the nth most recent line.
114+
//
115+
// Returns nil if there are not enough lines.
116+
func (p *Printer) Prev(n int) *Line {
117+
return &p.lines.PeekN(n + 1)[0]
118+
}
119+
120+
// NewLine pushes a new line.
121+
func (p *Printer) NewLine() {
122+
p.lines.Push(Line{})
123+
}
124+
125+
// Writes to the current line's buffer with Fprint.
126+
func (p *Printer) Write(args ...any) {
127+
fmt.Fprint(p.Current(), args...)
128+
}
129+
130+
// Writes to the current line's buffer with Fprintf.
131+
func (p *Printer) Writef(f string, args ...any) {
132+
fmt.Fprintf(p.Current(), f, args...)
133+
}
134+
135+
// Adds a new remark made from stringifying args.
136+
func (p *Printer) Remark(args ...any) {
137+
l := p.Current()
138+
l.remarks = append(l.remarks, fmt.Sprint(args...))
139+
}
140+
141+
// Adds a new remark made from stringifying args.
142+
func (p *Printer) Remarkf(f string, args ...any) {
143+
l := p.Current()
144+
l.remarks = append(l.remarks, fmt.Sprintf(f, args...))
145+
}
146+
147+
// Finish dumps the entire contents of the Printer into a byte array.
148+
func (p *Printer) Finish() []byte {
149+
if len(p.blocks) != 0 {
150+
panic("called Finish() without closing all blocks")
151+
}
152+
153+
var out bytes.Buffer
154+
indent := 0
155+
commentCol := -1
156+
commentColUntil := -1
157+
for i, line := range p.lines {
158+
if len(line.remarks) != 0 && commentColUntil < i {
159+
// Comments are aligned to the same column if they are contiguous, unless
160+
// crossing an indentation boundary would cause the remark column to be
161+
// further than it would have been without crossing the boundary.
162+
//
163+
// This allows the column finding algorithm to be linear.
164+
indent2 := indent
165+
commentCol = -1
166+
for j, line := range p.lines[i:] {
167+
if len(line.remarks) == 0 {
168+
commentColUntil = j + i
169+
break
170+
}
171+
172+
lineLen := indent2*p.Indent + utf8.RuneCount(line.Bytes())
173+
indent2 += line.indent
174+
if lineLen > commentCol {
175+
if j > 1 && line.indent != 0 {
176+
commentColUntil = j + i
177+
break
178+
}
179+
commentCol = lineLen
180+
}
181+
}
182+
if extra := commentCol % p.Indent; extra != 0 {
183+
commentCol += p.Indent - extra
184+
}
185+
}
186+
187+
for i := 0; i < indent*p.Indent; i++ {
188+
out.WriteString(" ")
189+
}
190+
191+
out.Write(line.Bytes())
192+
if len(line.remarks) > 0 {
193+
needed := commentCol - indent*p.Indent - line.Len()
194+
for i := 0; i < needed; i++ {
195+
out.WriteString(" ")
196+
}
197+
198+
out.WriteString(" # ")
199+
for i, remark := range line.remarks {
200+
if i != 0 {
201+
out.WriteString(", ")
202+
}
203+
out.WriteString(remark)
204+
}
205+
}
206+
207+
indent += line.indent
208+
out.WriteString("\n")
209+
}
210+
211+
return out.Bytes()
212+
}
213+
214+
type BlockInfo struct {
215+
// Whether this block will start and end with delimiters that do not need to
216+
// have spaces placed before/after them, allowing for output like {x} instead
217+
// of { x }.
218+
HasDelimiters bool
219+
// The maximum height of the block that will be folded into a single line.
220+
HeightToFoldAt int
221+
// The line (zero-indexed, starting from the last line) that should be the
222+
// final indented line. If there are not enough lines, the block is not
223+
// indented at all.
224+
UnindentAt int
225+
226+
start int
227+
}
228+
229+
// Starts a new indentation block.
230+
func (p *Printer) StartBlock(bi BlockInfo) {
231+
if p.Current().indent != 0 {
232+
panic("called StartBlock() too many times; this is a bug")
233+
}
234+
bi.start = len(p.lines) - 1
235+
p.blocks.Push(bi)
236+
p.Current().indent++
237+
}
238+
239+
// Discards the current block and undoes its indentation.
240+
func (p *Printer) DropBlock() *Line {
241+
bi := p.blocks.Pop()
242+
start := &p.lines[bi.start]
243+
start.indent--
244+
return start
245+
}
246+
247+
// Finishes an indentation block; a call to this function must match up to
248+
// a corresponding previous StartBlock() call. Returns the starting line for the
249+
// block.
250+
//
251+
// This function will perform folding of small blocks as appropriate.
252+
func (p *Printer) EndBlock() *Line {
253+
bi := p.blocks.Pop()
254+
start := &p.lines[bi.start]
255+
height := len(p.lines) - bi.start
256+
257+
// Does the unindentation operation. Because this may run after a successful
258+
// fold, we need to make sure that it re-computes the height.
259+
defer func() {
260+
height := len(p.lines) - bi.start
261+
if height <= bi.UnindentAt {
262+
p.lines[bi.start].indent--
263+
} else {
264+
p.lines.PeekN(bi.UnindentAt + 1)[0].indent--
265+
}
266+
}()
267+
268+
// Decide whether to fold this block.
269+
if height > bi.HeightToFoldAt || height < 2 {
270+
return start
271+
}
272+
273+
folds := 0
274+
remarks := 0
275+
for _, line := range p.lines[bi.start:] {
276+
folds += line.folds
277+
if len(line.remarks) > 0 {
278+
remarks++
279+
}
280+
}
281+
282+
if folds > p.MaxFolds {
283+
return start
284+
}
285+
286+
// Do not mix remarks from different lines.
287+
if remarks > 1 {
288+
return start
289+
}
290+
291+
// We are ok to unindent.
292+
for i, line := range p.lines[bi.start+1:] {
293+
if (i != 0 && i != height-2) || !bi.HasDelimiters {
294+
start.WriteString(" ")
295+
}
296+
start.Write(line.Bytes())
297+
if len(line.remarks) != 0 {
298+
// This will execute at most once per loop.
299+
start.remarks = line.remarks
300+
}
301+
}
302+
303+
start.folds = folds
304+
p.lines = p.lines[:bi.start+1]
305+
return start
306+
}

testdata/explicit-wire-types.pb.golden

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
8:I64 108i64
1010
9:I32 109i32
1111
10:I64 110i64
12-
11:I32 111.0i32 # 0x42de0000i32
13-
12:I64 112.0 # 0x405c000000000000i64
12+
11:I32 111.0i32 # 0x42de0000i32
13+
12:I64 112.0 # 0x405c000000000000i64
1414
13:VARINT 1
1515
14:LEN {"115"}
1616
15:LEN {"116"}
@@ -48,10 +48,10 @@
4848
39:I32 309i32
4949
40:I64 210i64
5050
40:I64 310i64
51-
41:I32 211.0i32 # 0x43530000i32
52-
41:I32 311.0i32 # 0x439b8000i32
53-
42:I64 212.0 # 0x406a800000000000i64
54-
42:I64 312.0 # 0x4073800000000000i64
51+
41:I32 211.0i32 # 0x43530000i32
52+
41:I32 311.0i32 # 0x439b8000i32
53+
42:I64 212.0 # 0x406a800000000000i64
54+
42:I64 312.0 # 0x4073800000000000i64
5555
43:VARINT 1
5656
43:VARINT 0
5757
44:LEN {"215"}
@@ -92,8 +92,8 @@
9292
68:I64 408i64
9393
69:I32 409i32
9494
70:I64 410i64
95-
71:I32 411.0i32 # 0x43cd8000i32
96-
72:I64 412.0 # 0x4079c00000000000i64
95+
71:I32 411.0i32 # 0x43cd8000i32
96+
72:I64 412.0 # 0x4079c00000000000i64
9797
73:VARINT 0
9898
74:LEN {"415"}
9999
75:LEN {"416"}

testdata/groups.pb.golden

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
4:EGROUP
1010
5: !{6: !{}}
1111
6: !{long-form:5}
12-
7: !{1: 1 long-form:5}
12+
7: !{
13+
1: 1
14+
long-form:5
15+
}
1316
7: !{
1417
1: 1
1518
1: 1

0 commit comments

Comments
 (0)