Skip to content

Commit c345915

Browse files
sbarzowskisparkprime
authored andcommitted
Location, error formatting and stack trace improvements (google#59)
* Location, error formatting and stack trace improvements * Static context for AST nodes * Thunks no longer need `name` * Prototype for showing snippets in error messages (old format still available) * Use ast.Function to represent methods and local function sugar. * Change tests so that the error output is pretty
1 parent a1b8248 commit c345915

File tree

145 files changed

+4662
-174
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

145 files changed

+4662
-174
lines changed

ast/ast.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,22 @@ type Identifiers []Identifier
3030

3131
// ---------------------------------------------------------------------------
3232

33+
type Context *string
34+
3335
type Node interface {
36+
Context() Context
3437
Loc() *LocationRange
3538
FreeVariables() Identifiers
3639
SetFreeVariables(Identifiers)
40+
SetContext(Context)
3741
}
3842
type Nodes []Node
3943

4044
// ---------------------------------------------------------------------------
4145

4246
type NodeBase struct {
4347
loc LocationRange
48+
context Context
4449
freeVariables Identifiers
4550
}
4651

@@ -70,6 +75,14 @@ func (n *NodeBase) SetFreeVariables(idents Identifiers) {
7075
n.freeVariables = idents
7176
}
7277

78+
func (n *NodeBase) Context() Context {
79+
return n.context
80+
}
81+
82+
func (n *NodeBase) SetContext(context Context) {
83+
n.context = context
84+
}
85+
7386
// ---------------------------------------------------------------------------
7487

7588
type IfSpec struct {
@@ -356,11 +369,9 @@ type Slice struct {
356369

357370
// LocalBind is a helper struct for astLocal
358371
type LocalBind struct {
359-
Variable Identifier
360-
Body Node
361-
FunctionSugar bool
362-
Params *Parameters // if functionSugar is true
363-
TrailingComma bool
372+
Variable Identifier
373+
Body Node
374+
Fun *Function
364375
}
365376
type LocalBinds []LocalBind
366377

@@ -439,7 +450,8 @@ type ObjectField struct {
439450
Hide ObjectFieldHide // (ignore if kind != astObjectField*)
440451
SuperSugar bool // +: (ignore if kind != astObjectField*)
441452
MethodSugar bool // f(x, y, z): ... (ignore if kind == astObjectAssert)
442-
Expr1 Node // Not in scope of the object
453+
Method *Function
454+
Expr1 Node // Not in scope of the object
443455
Id *Identifier
444456
Params *Parameters // If methodSugar == true then holds the params.
445457
TrailingComma bool // If methodSugar == true then remembers the trailing comma
@@ -448,12 +460,8 @@ type ObjectField struct {
448460

449461
// TODO(jbeda): Add the remaining constructor helpers here
450462

451-
func ObjectFieldLocal(methodSugar bool, id *Identifier, params *Parameters, trailingComma bool, body Node) ObjectField {
452-
return ObjectField{ObjectLocal, ObjectFieldVisible, false, methodSugar, nil, id, params, trailingComma, body, nil}
453-
}
454-
455463
func ObjectFieldLocalNoMethod(id *Identifier, body Node) ObjectField {
456-
return ObjectField{ObjectLocal, ObjectFieldVisible, false, false, nil, id, nil, false, body, nil}
464+
return ObjectField{ObjectLocal, ObjectFieldVisible, false, false, nil, nil, id, nil, false, body, nil}
457465
}
458466

459467
type ObjectFields []ObjectField

ast/location.go

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,22 @@ limitations under the License.
1616

1717
package ast
1818

19-
import "fmt"
19+
import (
20+
"bytes"
21+
"fmt"
22+
)
23+
24+
type Source struct {
25+
lines []string
26+
}
2027

2128
//////////////////////////////////////////////////////////////////////////////
2229
// Location
2330

2431
// Location represents a single location in an (unspecified) file.
2532
type Location struct {
26-
Line int
33+
Line int
34+
// Column is a byte offset from the beginning of the line
2735
Column int
2836
}
2937

@@ -36,14 +44,29 @@ func (l *Location) String() string {
3644
return fmt.Sprintf("%v:%v", l.Line, l.Column)
3745
}
3846

47+
func locationBefore(a Location, b Location) bool {
48+
if a.Line != b.Line {
49+
return a.Line < b.Line
50+
}
51+
return a.Column < b.Column
52+
}
53+
3954
//////////////////////////////////////////////////////////////////////////////
4055
// LocationRange
4156

4257
// LocationRange represents a range of a source file.
4358
type LocationRange struct {
4459
FileName string
4560
Begin Location
46-
End Location
61+
End Location // TODO(sbarzowski) inclusive? exclusive? a gap?
62+
file *Source
63+
}
64+
65+
func LocationRangeBetween(a, b *LocationRange) LocationRange {
66+
if a.file != b.file {
67+
panic("Cannot create a LocationRange between different files")
68+
}
69+
return MakeLocationRange(a.FileName, a.file, a.Begin, b.End)
4770
}
4871

4972
// IsSet returns if this LocationRange has been set.
@@ -70,11 +93,101 @@ func (lr *LocationRange) String() string {
7093
return fmt.Sprintf("%s(%v)-(%v)", filePrefix, lr.Begin.String(), lr.End.String())
7194
}
7295

96+
func (l *LocationRange) WithCode() bool {
97+
return l.Begin.Line != 0
98+
}
99+
73100
// This is useful for special locations, e.g. manifestation entry point.
74101
func MakeLocationRangeMessage(msg string) LocationRange {
75102
return LocationRange{FileName: msg}
76103
}
77104

78-
func MakeLocationRange(fn string, begin Location, end Location) LocationRange {
79-
return LocationRange{FileName: fn, Begin: begin, End: end}
105+
func MakeLocationRange(fn string, fc *Source, begin Location, end Location) LocationRange {
106+
return LocationRange{FileName: fn, file: fc, Begin: begin, End: end}
107+
}
108+
109+
type SourceProvider struct {
110+
}
111+
112+
func (sp *SourceProvider) GetSnippet(loc LocationRange) string {
113+
var result bytes.Buffer
114+
if loc.Begin.Line == 0 {
115+
return ""
116+
}
117+
for i := loc.Begin.Line; i <= loc.End.Line; i++ {
118+
inLineRange := trimToLine(loc, i)
119+
for j := inLineRange.Begin.Column; j < inLineRange.End.Column; j++ {
120+
result.WriteByte(loc.file.lines[i-1][j-1])
121+
}
122+
if i != loc.End.Line {
123+
result.WriteByte('\n')
124+
}
125+
}
126+
return result.String()
127+
}
128+
129+
func BuildSource(s string) *Source {
130+
var result []string
131+
var lineBuf bytes.Buffer
132+
for _, runeValue := range s {
133+
lineBuf.WriteRune(runeValue)
134+
if runeValue == '\n' {
135+
result = append(result, lineBuf.String())
136+
lineBuf.Reset()
137+
}
138+
}
139+
rest := lineBuf.String()
140+
// Stuff after last end-of-line (EOF or some more code)
141+
result = append(result, rest+"\n")
142+
return &Source{result}
143+
}
144+
145+
func trimToLine(loc LocationRange, line int) LocationRange {
146+
if loc.Begin.Line > line {
147+
panic("invalid")
148+
}
149+
if loc.Begin.Line != line {
150+
loc.Begin.Column = 1
151+
}
152+
loc.Begin.Line = line
153+
if loc.End.Line < line {
154+
panic("invalid")
155+
}
156+
if loc.End.Line != line {
157+
loc.End.Column = len(loc.file.lines[line-1])
158+
}
159+
loc.End.Line = line
160+
return loc
161+
}
162+
163+
// lineBeginning returns a part of the line directly before LocationRange
164+
// for example:
165+
// local x = foo()
166+
// ^^^^^ <- LocationRange loc
167+
// then
168+
// local x = foo()
169+
// ^^^^^^^^^^ <- lineBeginning(loc)
170+
func LineBeginning(loc *LocationRange) LocationRange {
171+
return LocationRange{
172+
Begin: Location{Line: loc.Begin.Line, Column: 1},
173+
End: loc.Begin,
174+
FileName: loc.FileName,
175+
file: loc.file,
176+
}
177+
}
178+
179+
// lineEnding returns a part of the line directly after LocationRange
180+
// for example:
181+
// local x = foo() + test
182+
// ^^^^^ <- LocationRange loc
183+
// then
184+
// local x = foo() + test
185+
// ^^^^^^^ <- lineEnding(loc)
186+
func LineEnding(loc *LocationRange) LocationRange {
187+
return LocationRange{
188+
Begin: loc.End,
189+
End: Location{Line: loc.End.Line, Column: len(loc.file.lines[loc.End.Line-1])},
190+
FileName: loc.FileName,
191+
file: loc.file,
192+
}
80193
}

builtins.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ type UnaryBuiltin struct {
608608

609609
func getBuiltinEvaluator(e *evaluator, name ast.Identifier) *evaluator {
610610
loc := ast.MakeLocationRangeMessage("<builtin>")
611-
context := TraceContext{Name: "builtin function <" + string(name) + ">"}
611+
context := "builtin function <" + string(name) + ">"
612612
trace := TraceElement{loc: &loc, context: &context}
613613
return &evaluator{i: e.i, trace: &trace}
614614
}

desugarer.go

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -128,22 +128,14 @@ func desugarFields(location ast.LocationRange, fields *ast.ObjectFields, objLeve
128128
field.Expr2 = assertion
129129
}
130130

131-
// Remove methods
132131
for i := range *fields {
133132
field := &((*fields)[i])
134-
if !field.MethodSugar {
133+
if field.Method == nil {
135134
continue
136135
}
137-
origBody := field.Expr2
138-
function := &ast.Function{
139-
// TODO(sbarzowski) better location
140-
NodeBase: ast.NewNodeBaseLoc(*origBody.Loc()),
141-
Parameters: *field.Params,
142-
Body: origBody,
143-
}
144-
field.MethodSugar = false
145-
field.Params = nil
146-
field.Expr2 = function
136+
field.Expr2 = field.Method
137+
field.Method = nil
138+
// Body of the function already desugared through expr2
147139
}
148140

149141
// Remove object-level locals
@@ -483,19 +475,11 @@ func desugar(astPtr *ast.Node, objLevel int) (err error) {
483475

484476
case *ast.Local:
485477
for i := range node.Binds {
486-
if node.Binds[i].FunctionSugar {
487-
origBody := node.Binds[i].Body
488-
function := &ast.Function{
489-
// TODO(sbarzowski) better location
490-
NodeBase: ast.NewNodeBaseLoc(*origBody.Loc()),
491-
Parameters: *node.Binds[i].Params,
492-
Body: origBody,
493-
}
478+
if node.Binds[i].Fun != nil {
494479
node.Binds[i] = ast.LocalBind{
495-
Variable: node.Binds[i].Variable,
496-
Body: function,
497-
FunctionSugar: false,
498-
Params: nil,
480+
Variable: node.Binds[i].Variable,
481+
Body: node.Binds[i].Fun,
482+
Fun: nil,
499483
}
500484
}
501485
err = desugar(&node.Binds[i].Body, objLevel)

error_formatter.go

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"bytes"
2020
"fmt"
2121

22+
"github.com/fatih/color"
2223
"github.com/google/go-jsonnet/ast"
2324
"github.com/google/go-jsonnet/parser"
2425
)
@@ -27,10 +28,13 @@ type ErrorFormatter struct {
2728
// TODO(sbarzowski) use this
2829
// MaxStackTraceSize is the maximum length of stack trace before cropping
2930
MaxStackTraceSize int
30-
// TODO(sbarzowski) use these
31+
32+
// Examples of current state of the art.
33+
// http://elm-lang.org/blog/compiler-errors-for-humans
34+
// https://clang.llvm.org/diagnostics.html
3135
pretty bool
3236
colorful bool
33-
SP SourceProvider
37+
SP *ast.SourceProvider
3438
}
3539

3640
func (ef *ErrorFormatter) format(err error) string {
@@ -46,12 +50,13 @@ func (ef *ErrorFormatter) format(err error) string {
4650

4751
func (ef *ErrorFormatter) formatRuntime(err *RuntimeError) string {
4852
return err.Error() + "\n" + ef.buildStackTrace(err.StackTrace)
49-
// TODO(sbarzowski) pretty stuff
5053
}
5154

5255
func (ef *ErrorFormatter) formatStatic(err *parser.StaticError) string {
53-
return err.Error() + "\n"
54-
// TODO(sbarzowski) pretty stuff
56+
var buf bytes.Buffer
57+
buf.WriteString(err.Error() + "\n")
58+
ef.showCode(&buf, err.Loc)
59+
return buf.String()
5560
}
5661

5762
const bugURL = "https://github.com/google/go-jsonnet/issues"
@@ -61,19 +66,42 @@ func (ef *ErrorFormatter) formatInternal(err error) string {
6166
"Please report a bug here: " + bugURL + "\n"
6267
}
6368

69+
func (ef *ErrorFormatter) showCode(buf *bytes.Buffer, loc ast.LocationRange) {
70+
errFprintf := fmt.Fprintf
71+
if ef.colorful {
72+
errFprintf = color.New(color.FgRed).Fprintf
73+
}
74+
if loc.WithCode() {
75+
// TODO(sbarzowski) include line numbers
76+
// TODO(sbarzowski) underline errors instead of depending only on color
77+
fmt.Fprintf(buf, "\n")
78+
beginning := ast.LineBeginning(&loc)
79+
ending := ast.LineEnding(&loc)
80+
fmt.Fprintf(buf, "%v", ef.SP.GetSnippet(beginning))
81+
errFprintf(buf, "%v", ef.SP.GetSnippet(loc))
82+
fmt.Fprintf(buf, "%v", ef.SP.GetSnippet(ending))
83+
buf.WriteByte('\n')
84+
}
85+
fmt.Fprintf(buf, "\n")
86+
}
87+
6488
func (ef *ErrorFormatter) buildStackTrace(frames []TraceFrame) string {
6589
// https://github.com/google/jsonnet/blob/master/core/libjsonnet.cpp#L594
6690
var buf bytes.Buffer
67-
for _, f := range frames {
91+
for i := len(frames) - 1; i >= 0; i-- {
92+
f := frames[i]
93+
// TODO(sbarzowski) make pretty format more readable (it's already useful)
94+
if ef.pretty {
95+
fmt.Fprintf(&buf, "-------------------------------------------------\n")
96+
}
97+
// TODO(sbarzowski) tabs are probably a bad idea
6898
fmt.Fprintf(&buf, "\t%v\t%v\n", &f.Loc, f.Name)
99+
if ef.pretty {
100+
ef.showCode(&buf, f.Loc)
101+
}
102+
69103
// TODO(sbarzowski) handle max stack trace size
70104
// TODO(sbarzowski) I think the order of frames is reversed
71105
}
72106
return buf.String()
73107
}
74-
75-
type SourceProvider interface {
76-
// TODO(sbarzowski) problem: locationRange.FileName may not necessarily
77-
// uniquely identify a file. But this is the interface we want to have here.
78-
getCode(ast.LocationRange) string
79-
}

0 commit comments

Comments
 (0)