Better stacktrace for manifestation, +: and object assertions.

Fixes #282
This commit is contained in:
Stanisław Barzowski 2020-12-13 18:02:53 +01:00
parent 7d3bda3911
commit 6140a2f75a
29 changed files with 220 additions and 42 deletions

View File

@ -62,7 +62,12 @@ func desugarFields(nodeBase ast.NodeBase, fields *ast.ObjectFields, objLevel int
if msg == nil {
msg = buildLiteralString("Object assertion failed.")
}
onFailure := &ast.Error{Expr: msg}
onFailure := &ast.Error{
NodeBase: ast.NodeBase{
LocRange: field.LocRange,
},
Expr: msg,
}
asserts = append(asserts, &ast.Conditional{
NodeBase: ast.NodeBase{
LocRange: field.LocRange,

View File

@ -35,9 +35,9 @@ type environment struct {
// Bindings introduced in this frame. The way previous bindings are treated
// depends on the type of a frame.
// If isCall == true then previous bindings are ignored (it's a clean
// If cleanEnv == true then previous bindings are ignored (it's a clean
// environment with just the variables we have here).
// If isCall == false then if this frame doesn't contain a binding
// If cleanEnv == false then if this frame doesn't contain a binding
// previous bindings will be used.
upValues bindingFrame
}
@ -52,7 +52,7 @@ func makeEnvironment(upValues bindingFrame, sb selfBinding) environment {
func (i *interpreter) getCurrentStackTrace() []traceFrame {
var result []traceFrame
for _, f := range i.stack.stack {
if f.isCall {
if f.cleanEnv {
result = append(result, traceElementToTraceFrame(f.trace))
}
}
@ -65,8 +65,7 @@ func (i *interpreter) getCurrentStackTrace() []traceFrame {
type callFrame struct {
// True if it switches to a clean environment (function call or array element)
// False otherwise, e.g. for local
// This makes callFrame a misnomer as it is technically not always a call...
isCall bool
cleanEnv bool
// Tracing information about the place where it was called from.
trace traceElement
@ -87,8 +86,8 @@ func dumpCallFrame(c *callFrame) string {
} else {
loc = *c.trace.loc
}
return fmt.Sprintf("<callFrame isCall = %t location = %v trimmable = %t>",
c.isCall,
return fmt.Sprintf("<callFrame cleanEnv = %t location = %v trimmable = %t>",
c.cleanEnv,
loc,
c.trimmable,
)
@ -121,7 +120,7 @@ func (s *callStack) top() *callFrame {
// of the frame we want to pop.
func (s *callStack) popIfExists(whichFrame int) {
if len(s.stack) == whichFrame {
if s.top().isCall {
if s.top().cleanEnv {
s.calls--
}
s.setCurrentTrace(s.stack[len(s.stack)-1].trace)
@ -132,7 +131,7 @@ func (s *callStack) popIfExists(whichFrame int) {
/** If there is a trimmable frame followed by some locals, pop them all. */
func (s *callStack) tailCallTrimStack() {
for i := len(s.stack) - 1; i >= 0; i-- {
if s.stack[i].isCall {
if s.stack[i].cleanEnv {
if !s.stack[i].trimmable {
return
}
@ -167,7 +166,7 @@ func (s *callStack) newCall(env environment, trimmable bool) {
panic("Saving empty traceElement on stack")
}
s.stack = append(s.stack, &callFrame{
isCall: true,
cleanEnv: true,
trace: s.currentTrace,
env: env,
trimmable: trimmable,
@ -187,7 +186,7 @@ func (s *callStack) newLocal(vars bindingFrame) {
// getSelfBinding resolves the self construct
func (s *callStack) getSelfBinding() selfBinding {
for i := len(s.stack) - 1; i >= 0; i-- {
if s.stack[i].isCall {
if s.stack[i].cleanEnv {
return s.stack[i].env.selfBinding
}
}
@ -201,7 +200,7 @@ func (s *callStack) lookUpVar(id ast.Identifier) *cachedThunk {
if present {
return bind
}
if s.stack[i].isCall {
if s.stack[i].cleanEnv {
// Nothing beyond the captured environment of the thunk / closure.
break
}
@ -654,6 +653,15 @@ func (i *interpreter) manifestJSON(v value) (interface{}, error) {
if i.stack.currentTrace == (traceElement{}) {
panic("manifesting JSON with empty traceElement")
}
// Fresh frame for better stack traces
err := i.newCall(environment{}, false)
if err != nil {
return nil, err
}
stackSize := len(i.stack.stack)
defer i.stack.popIfExists(stackSize)
switch v := v.(type) {
case *valueBoolean:
@ -673,16 +681,23 @@ func (i *interpreter) manifestJSON(v value) (interface{}, error) {
case *valueArray:
result := make([]interface{}, 0, len(v.elements))
for _, th := range v.elements {
for index, th := range v.elements {
msg := ast.MakeLocationRangeMessage(fmt.Sprintf("Array element %d", index))
i.stack.setCurrentTrace(traceElement{
loc: &msg,
})
elVal, err := i.evaluatePV(th)
if err != nil {
i.stack.clearCurrentTrace()
return nil, err
}
elem, err := i.manifestJSON(elVal)
if err != nil {
i.stack.clearCurrentTrace()
return nil, err
}
result = append(result, elem)
i.stack.clearCurrentTrace()
}
return result, nil
@ -690,24 +705,37 @@ func (i *interpreter) manifestJSON(v value) (interface{}, error) {
fieldNames := objectFields(v, withoutHidden)
sort.Strings(fieldNames)
msg := ast.MakeLocationRangeMessage("Checking object assertions")
i.stack.setCurrentTrace(traceElement{
loc: &msg,
})
err := checkAssertions(i, v)
if err != nil {
i.stack.clearCurrentTrace()
return nil, err
}
i.stack.clearCurrentTrace()
result := make(map[string]interface{})
for _, fieldName := range fieldNames {
msg := ast.MakeLocationRangeMessage(fmt.Sprintf("Field %#v", fieldName))
i.stack.setCurrentTrace(traceElement{
loc: &msg,
})
fieldVal, err := v.index(i, fieldName)
if err != nil {
i.stack.clearCurrentTrace()
return nil, err
}
field, err := i.manifestJSON(fieldVal)
if err != nil {
i.stack.clearCurrentTrace()
return nil, err
}
result[fieldName] = field
i.stack.clearCurrentTrace()
}
return result, nil
@ -944,10 +972,13 @@ func (i *interpreter) EvalInCleanEnv(env *environment, ast ast.Node, trimmable b
stackSize := len(i.stack.stack)
val, err := i.evaluate(ast, tailCall)
if err != nil {
return nil, err
}
i.stack.popIfExists(stackSize)
return val, err
return val, nil
}
func (i *interpreter) evaluatePV(ph potentialValue) (value, error) {
@ -1211,7 +1242,14 @@ func makeInitialEnv(filename string, baseStd *valueObject) environment {
)
}
func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, traceElement, error) {
func manifestationTrace() traceElement {
manifestationLoc := ast.MakeLocationRangeMessage("During manifestation")
return traceElement{
loc: &manifestationLoc,
}
}
func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, error) {
evalLoc := ast.MakeLocationRangeMessage("During evaluation")
evalTrace := traceElement{
loc: &evalLoc,
@ -1221,7 +1259,7 @@ func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, traceEleme
result, err := i.EvalInCleanEnv(&env, node, false)
i.stack.clearCurrentTrace()
if err != nil {
return nil, traceElement{}, err
return nil, err
}
// If it's not a function, ignore TLA
if f, ok := result.(*valueFunction); ok {
@ -1238,14 +1276,10 @@ func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, traceEleme
result, err = f.call(i, args)
i.stack.clearCurrentTrace()
if err != nil {
return nil, traceElement{}, err
return nil, err
}
}
manifestationLoc := ast.MakeLocationRangeMessage("During manifestation")
manifestationTrace := traceElement{
loc: &manifestationLoc,
}
return result, manifestationTrace, nil
return result, nil
}
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
@ -1257,13 +1291,13 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]
return "", err
}
result, manifestationTrace, err := evaluateAux(i, node, tla)
result, err := evaluateAux(i, node, tla)
if err != nil {
return "", err
}
var buf bytes.Buffer
i.stack.setCurrentTrace(manifestationTrace)
i.stack.setCurrentTrace(manifestationTrace())
if stringOutputMode {
err = i.manifestString(&buf, result)
} else {
@ -1286,12 +1320,12 @@ func evaluateMulti(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[st
return nil, err
}
result, manifestationTrace, err := evaluateAux(i, node, tla)
result, err := evaluateAux(i, node, tla)
if err != nil {
return nil, err
}
i.stack.setCurrentTrace(manifestationTrace)
i.stack.setCurrentTrace(manifestationTrace())
manifested, err := i.manifestAndSerializeMulti(result, stringOutputMode)
i.stack.clearCurrentTrace()
return manifested, err
@ -1306,12 +1340,12 @@ func evaluateStream(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[s
return nil, err
}
result, manifestationTrace, err := evaluateAux(i, node, tla)
result, err := evaluateAux(i, node, tla)
if err != nil {
return nil, err
}
i.stack.setCurrentTrace(manifestationTrace)
i.stack.setCurrentTrace(manifestationTrace())
manifested, err := i.manifestAndSerializeYAMLStream(result)
i.stack.clearCurrentTrace()
return manifested, err

View File

@ -124,7 +124,7 @@ func prepareStdlib(g *typeGraph) {
"setInter": g.newFuncType(anyArrayType, []ast.Parameter{required("a"), required("b"), optional("keyF")}),
"setUnion": g.newFuncType(anyArrayType, []ast.Parameter{required("a"), required("b"), optional("keyF")}),
"setDiff": g.newFuncType(anyArrayType, []ast.Parameter{required("a"), required("b"), optional("keyF")}),
"setMember": g.newFuncType(anyArrayType, []ast.Parameter{required("x"), required("arr"), optional("keyF")}),
"setMember": g.newFuncType(boolType, []ast.Parameter{required("x"), required("arr"), optional("keyF")}),
// Encoding

View File

@ -0,0 +1 @@
!std.setMember([1, 2, 3], 1)

View File

View File

@ -9,6 +9,9 @@ RUNTIME ERROR: xxx
{ local foo(bar) = error bar, baz: foo("xxx") }
-------------------------------------------------
Field "baz"
-------------------------------------------------
During manifestation

View File

@ -1,4 +1,7 @@
RUNTIME ERROR: couldn't manifest function as JSON
-------------------------------------------------
Field "f"
-------------------------------------------------
During manifestation

View File

@ -4,6 +4,9 @@ RUNTIME ERROR: Attempt to use super when there is no super class.
{ x: super.x }
-------------------------------------------------
Field "x"
-------------------------------------------------
During manifestation

View File

@ -4,6 +4,9 @@ RUNTIME ERROR: xxx
{ ["x"]: error "xxx" for x in [1] }
-------------------------------------------------
Field "x"
-------------------------------------------------
During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: Object assertion failed.
-------------------------------------------------
testdata/object_invariant10:1:16-28
{ assert true, assert false }
-------------------------------------------------
Checking object assertions
-------------------------------------------------
During manifestation

View File

@ -1,6 +1,8 @@
RUNTIME ERROR: Object assertion failed.
-------------------------------------------------
testdata/object_invariant11:1:3-15
{ assert false }.x
-------------------------------------------------
testdata/object_invariant11:1:1-19 $

View File

@ -4,6 +4,9 @@ RUNTIME ERROR: x
{ assert error "x" }
-------------------------------------------------
Checking object assertions
-------------------------------------------------
During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: xxx
-------------------------------------------------
testdata/object_invariant14:1:3-22
{ assert false: "xxx" }
-------------------------------------------------
Checking object assertions
-------------------------------------------------
During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: Object assertion failed.
-------------------------------------------------
testdata/object_invariant2:1:3-15
{ assert false }
-------------------------------------------------
Checking object assertions
-------------------------------------------------
During manifestation

View File

@ -4,6 +4,9 @@ RUNTIME ERROR: Attempt to use super when there is no super class.
{ x: 5, assert super.x == 5 }
-------------------------------------------------
Checking object assertions
-------------------------------------------------
During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: Object assertion failed.
-------------------------------------------------
testdata/object_invariant8:1:9-27
{ x: 5, assert self.x == 4 }
-------------------------------------------------
Checking object assertions
-------------------------------------------------
During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: Object assertion failed.
-------------------------------------------------
testdata/object_invariant9:1:16-28
{ assert true, assert false }
-------------------------------------------------
Checking object assertions
-------------------------------------------------
During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: Object assertion failed.
-------------------------------------------------
testdata/object_invariant_plus:1:2-14
{assert false} + {assert true}
-------------------------------------------------
Checking object assertions
-------------------------------------------------
During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: Object assertion failed.
-------------------------------------------------
testdata/object_invariant_plus2:1:18-30
{assert true} + {assert false}
-------------------------------------------------
Checking object assertions
-------------------------------------------------
During manifestation

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: yyy
-------------------------------------------------
testdata/object_invariant_plus6:1:27-46
{ assert false: "xxx" } { assert false: "yyy" }
-------------------------------------------------
Checking object assertions
-------------------------------------------------
During manifestation

13
testdata/stacktrace_assert.golden vendored Normal file
View File

@ -0,0 +1,13 @@
RUNTIME ERROR: Object assertion failed.
-------------------------------------------------
testdata/stacktrace_assert:1:3-15
{ assert false }
-------------------------------------------------
Checking object assertions
-------------------------------------------------
During manifestation

1
testdata/stacktrace_assert.jsonnet vendored Normal file
View File

@ -0,0 +1 @@
{ assert false }

View File

13
testdata/stacktrace_plussuper.golden vendored Normal file
View File

@ -0,0 +1,13 @@
RUNTIME ERROR: Unexpected type null
-------------------------------------------------
testdata/stacktrace_plussuper:2:7-9 +:
a+: {},
-------------------------------------------------
Field "a"
-------------------------------------------------
During manifestation

5
testdata/stacktrace_plussuper.jsonnet vendored Normal file
View File

@ -0,0 +1,5 @@
local a(input) = input + {
a+: {},
};
a({a: null})

View File

View File

@ -1,6 +1,11 @@
RUNTIME ERROR: Object assertion failed.
-------------------------------------------------
testdata/supersugar8:1:3-16
{ assert self.x } { x +: false }
-------------------------------------------------
Checking object assertions
-------------------------------------------------
During manifestation

View File

@ -36,6 +36,10 @@ func (rv *readyValue) evaluate(i *interpreter, sb selfBinding, origBinding bindi
return rv.content, nil
}
func (rv *readyValue) loc() *ast.LocationRange {
return &ast.LocationRange{}
}
// potentialValues
// -------------------------------------
@ -89,7 +93,12 @@ type codeUnboundField struct {
func (f *codeUnboundField) evaluate(i *interpreter, sb selfBinding, origBindings bindingFrame, fieldName string) (value, error) {
env := makeEnvironment(origBindings, sb)
return i.EvalInCleanEnv(&env, f.body, false)
val, err := i.EvalInCleanEnv(&env, f.body, false)
return val, err
}
func (f *codeUnboundField) loc() *ast.LocationRange {
return f.body.Loc()
}
// Provide additional bindings for a field. It shadows bindings from the object.
@ -110,24 +119,54 @@ func (f *bindingsUnboundField) evaluate(i *interpreter, sb selfBinding, origBind
return f.inner.evaluate(i, sb, upValues, fieldName)
}
func (f *bindingsUnboundField) loc() *ast.LocationRange {
return f.inner.loc()
}
// plusSuperUnboundField represents a `field+: ...` that hasn't been bound to an object.
type plusSuperUnboundField struct {
inner unboundField
}
func (f *plusSuperUnboundField) evaluate(i *interpreter, sb selfBinding, origBinding bindingFrame, fieldName string) (value, error) {
err := i.newCall(environment{}, false)
if err != nil {
return nil, err
}
stackSize := len(i.stack.stack)
defer i.stack.popIfExists(stackSize)
context := "+:"
i.stack.setCurrentTrace(traceElement{
loc: f.loc(),
context: &context,
})
defer i.stack.clearCurrentTrace()
right, err := f.inner.evaluate(i, sb, origBinding, fieldName)
if err != nil {
return nil, err
}
if !objectHasField(sb.super(), fieldName, withHidden) {
return right, nil
}
left, err := objectIndex(i, sb.super(), fieldName)
if err != nil {
return nil, err
}
return builtinPlus(i, left, right)
value, err := builtinPlus(i, left, right)
if err != nil {
return nil, err
}
return value, nil
}
func (f *plusSuperUnboundField) loc() *ast.LocationRange {
return f.inner.loc()
}
// evalCallables

View File

@ -572,6 +572,7 @@ func checkAssertionsHelper(i *interpreter, obj *valueObject, curr uncachedObject
default:
panic(fmt.Sprintf("Unknown object type %#v", curr))
}
}
func checkAssertions(i *interpreter, obj *valueObject) error {
@ -611,6 +612,7 @@ type simpleObjectField struct {
// unboundField is a field that doesn't know yet in which object it is.
type unboundField interface {
evaluate(i *interpreter, sb selfBinding, origBinding bindingFrame, fieldName string) (value, error)
loc() *ast.LocationRange
}
// extendedObject represents an object created through inheritance (left + right).