From 36b4a6f848166189ebd4086de67a4fc2c7a6bda2 Mon Sep 17 00:00:00 2001 From: joeduffy Date: Sun, 12 Feb 2017 09:38:19 -0800 Subject: [PATCH] Implement stack traces This change implements stack traces. This is primarily so that we can print out a full stack trace in the face of an unhandled exception, and is done simply by recording the full trace during evaluation alongside the existing local variable scopes. --- pkg/diag/sink.go | 76 +++++++++++++++++++++++------------- pkg/eval/alloc.go | 6 ++- pkg/eval/eval.go | 88 ++++++++++++++++++++++++------------------ pkg/eval/exceptions.go | 20 +++++++--- pkg/eval/rt/object.go | 47 ++++++++++++++-------- pkg/eval/rt/stack.go | 54 ++++++++++++++++++++++++++ 6 files changed, 202 insertions(+), 89 deletions(-) create mode 100644 pkg/eval/rt/stack.go diff --git a/pkg/diag/sink.go b/pkg/diag/sink.go index 0e75635b9..23395c96a 100644 --- a/pkg/diag/sink.go +++ b/pkg/diag/sink.go @@ -34,6 +34,8 @@ type Sink interface { // Stringify stringifies a diagnostic in the usual way (e.g., "error: MU123: Mu.yaml:7:39: error goes here\n"). Stringify(diag *Diag, cat Category, args ...interface{}) string + // StringifyLocation stringifies a source document location. + StringifyLocation(doc *Document, loc *Location) string } // Category dictates the kind of diagnostic. @@ -108,33 +110,9 @@ func (d *defaultSink) Stringify(diag *Diag, cat Category, args ...interface{}) s var buffer bytes.Buffer // First print the location if there is one. - if diag.Doc != nil { - if d.opts.Colors { - buffer.WriteString("{fg 6}") // cyan - } - - file := diag.Doc.File - if d.opts.Pwd != "" { - // If a PWD is available, try to create a relative path. - rel, err := filepath.Rel(d.opts.Pwd, file) - if err == nil { - file = rel - } - } - buffer.WriteString(file) - - if diag.Loc != nil && !diag.Loc.IsEmpty() { - buffer.WriteRune('(') - buffer.WriteString(strconv.Itoa(diag.Loc.Start.Line)) - buffer.WriteRune(',') - buffer.WriteString(strconv.Itoa(diag.Loc.Start.Column)) - buffer.WriteRune(')') - } + if diag.Doc != nil || diag.Loc != nil { + buffer.WriteString(d.StringifyLocation(diag.Doc, diag.Loc)) buffer.WriteString(": ") - - if d.opts.Colors { - buffer.WriteString("{reset}") - } } // Now print the message category's prefix (error/warning). @@ -190,3 +168,49 @@ func (d *defaultSink) Stringify(diag *Diag, cat Category, args ...interface{}) s return s } + +func (d *defaultSink) StringifyLocation(doc *Document, loc *Location) string { + var buffer bytes.Buffer + + if doc != nil { + if d.opts.Colors { + buffer.WriteString("{fg 6}") // cyan + } + + file := doc.File + if d.opts.Pwd != "" { + // If a PWD is available, try to create a relative path. + rel, err := filepath.Rel(d.opts.Pwd, file) + if err == nil { + file = rel + } + } + buffer.WriteString(file) + } + + if loc != nil && !loc.IsEmpty() { + buffer.WriteRune('(') + buffer.WriteString(strconv.Itoa(loc.Start.Line)) + buffer.WriteRune(',') + buffer.WriteString(strconv.Itoa(loc.Start.Column)) + buffer.WriteRune(')') + } + + var s string + if doc != nil || loc != nil { + if d.opts.Colors { + buffer.WriteString("{reset}") + } + + s = buffer.String() + + // If colorization was requested, compile and execute the directives now. + if d.opts.Colors { + var err error + s, err = loreley.CompileAndExecuteToString(s, nil, nil) + contract.Assert(err == nil) + } + } + + return s +} diff --git a/pkg/eval/alloc.go b/pkg/eval/alloc.go index 35f26689b..3b8e667e4 100644 --- a/pkg/eval/alloc.go +++ b/pkg/eval/alloc.go @@ -4,6 +4,7 @@ package eval import ( "github.com/marapongo/mu/pkg/compiler/symbols" + "github.com/marapongo/mu/pkg/diag" "github.com/marapongo/mu/pkg/eval/rt" ) @@ -81,8 +82,9 @@ func (a *Allocator) NewPointer(t symbols.Type, ptr *rt.Pointer) *rt.Object { } // NewException creates a new exception with the given message. -func (a *Allocator) NewException(message string, args ...interface{}) *rt.Object { - obj := rt.NewExceptionObject(message, args...) +func (a *Allocator) NewException(node diag.Diagable, stack *rt.StackFrame, + message string, args ...interface{}) *rt.Object { + obj := rt.NewExceptionObject(node, stack, message, args...) a.onNewObject(obj) return obj } diff --git a/pkg/eval/eval.go b/pkg/eval/eval.go index b18f79870..6d323b639 100644 --- a/pkg/eval/eval.go +++ b/pkg/eval/eval.go @@ -16,7 +16,6 @@ import ( "github.com/marapongo/mu/pkg/compiler/types" "github.com/marapongo/mu/pkg/diag" "github.com/marapongo/mu/pkg/eval/rt" - "github.com/marapongo/mu/pkg/graph" "github.com/marapongo/mu/pkg/tokens" "github.com/marapongo/mu/pkg/util/contract" ) @@ -28,11 +27,11 @@ type Interpreter interface { Ctx() *binder.Context // the binding context object. // EvaluatePackage performs evaluation on the given blueprint package. - EvaluatePackage(pkg *symbols.Package, args core.Args) graph.Graph + EvaluatePackage(pkg *symbols.Package, args core.Args) // EvaluateModule performs evaluation on the given module's entrypoint function. - EvaluateModule(mod *symbols.Module, args core.Args) graph.Graph - // EvaluateFunction performs an evaluation of the given function, using the provided arguments, returning its graph. - EvaluateFunction(fnc symbols.Function, this *rt.Object, args core.Args) graph.Graph + EvaluateModule(mod *symbols.Module, args core.Args) + // EvaluateFunction performs an evaluation of the given function, using the provided arguments. + EvaluateFunction(fnc symbols.Function, this *rt.Object, args core.Args) } // InterpreterHooks is a set of callbacks that can be used to hook into interesting interpreter events. @@ -68,6 +67,7 @@ type evaluator struct { hooks InterpreterHooks // callbacks that hook into interpreter events. alloc *Allocator // the object allocator. globals globalMap // the object values for global variable symbols. + stack *rt.StackFrame // a stack of frames to keep track of calls. locals *localScope // local variable values scoped by the lexical structure. modinits modinitMap // a map of which modules have been initialized already. classinits classinitMap // a map of which classes have been initialized already. @@ -83,7 +83,7 @@ func (e *evaluator) Ctx() *binder.Context { return e.ctx } func (e *evaluator) Diag() diag.Sink { return e.ctx.Diag } // EvaluatePackage performs evaluation on the given blueprint package. -func (e *evaluator) EvaluatePackage(pkg *symbols.Package, args core.Args) graph.Graph { +func (e *evaluator) EvaluatePackage(pkg *symbols.Package, args core.Args) { glog.Infof("Evaluating package '%v'", pkg.Name()) if e.hooks != nil { e.hooks.OnEnterPackage(pkg) @@ -99,18 +99,17 @@ func (e *evaluator) EvaluatePackage(pkg *symbols.Package, args core.Args) graph. // Search the package for a default module "index" to evaluate. defmod := pkg.Default() - if defmod != nil { + if defmod == nil { + e.Diag().Errorf(errors.ErrorPackageHasNoDefaultModule.At(pkg.Tree()), pkg.Name()) + } else { mod := pkg.Modules[*defmod] contract.Assert(mod != nil) - return e.EvaluateModule(mod, args) + e.EvaluateModule(mod, args) } - - e.Diag().Errorf(errors.ErrorPackageHasNoDefaultModule.At(pkg.Tree()), pkg.Name()) - return nil } // EvaluateModule performs evaluation on the given module's entrypoint function. -func (e *evaluator) EvaluateModule(mod *symbols.Module, args core.Args) graph.Graph { +func (e *evaluator) EvaluateModule(mod *symbols.Module, args core.Args) { glog.Infof("Evaluating module '%v'", mod.Token()) if e.hooks != nil { e.hooks.OnEnterModule(mod) @@ -125,18 +124,21 @@ func (e *evaluator) EvaluateModule(mod *symbols.Module, args core.Args) graph.Gr } // Fetch the module's entrypoint function, erroring out if it doesn't have one. - if ep, has := mod.Members[tokens.EntryPointFunction]; has { - if epfnc, ok := ep.(symbols.Function); ok { - return e.EvaluateFunction(epfnc, nil, args) + hadEntry := false + if entry, has := mod.Members[tokens.EntryPointFunction]; has { + if entryfnc, ok := entry.(symbols.Function); ok { + e.EvaluateFunction(entryfnc, nil, args) + hadEntry = true } } - e.Diag().Errorf(errors.ErrorModuleHasNoEntryPoint.At(mod.Tree()), mod.Name()) - return nil + if !hadEntry { + e.Diag().Errorf(errors.ErrorModuleHasNoEntryPoint.At(mod.Tree()), mod.Name()) + } } // EvaluateFunction performs an evaluation of the given function, using the provided arguments, returning its graph. -func (e *evaluator) EvaluateFunction(fnc symbols.Function, this *rt.Object, args core.Args) graph.Graph { +func (e *evaluator) EvaluateFunction(fnc symbols.Function, this *rt.Object, args core.Args) { glog.Infof("Evaluating function '%v'", fnc.Token()) if e.hooks != nil { e.hooks.OnEnterFunction(fnc) @@ -181,7 +183,7 @@ func (e *evaluator) EvaluateFunction(fnc symbols.Function, this *rt.Object, args argo := e.alloc.NewConstant(arg) if !types.CanConvert(argo.Type(), ptys[i]) { e.Diag().Errorf(errors.ErrorFunctionArgIncorrectType.At(fnc.Tree()), ptys[i], argo.Type()) - return nil + break } argos = append(argos, argo) } else { @@ -206,9 +208,6 @@ func (e *evaluator) EvaluateFunction(fnc symbols.Function, this *rt.Object, args // Dump the evaluation state at log-level 5, if it is enabled. e.dumpEvalState(5) - - // TODO: turn the returned object into a graph. - return nil } // Utility functions @@ -288,26 +287,38 @@ func (e *evaluator) ensureModuleInit(mod *symbols.Module) { // issueUnhandledException issues an unhandled exception error using the given diagnostic and unwind information. func (e *evaluator) issueUnhandledException(uw *Unwind, err *diag.Diag, args ...interface{}) { contract.Assert(uw.Throw()) + + // Produce a message with the exception text plus stack trace. var msg string if thrown := uw.Thrown(); thrown != nil { - msg = thrown.ExceptionMessage() - } - if msg == "" { + info := thrown.ExceptionValue() + msg = info.Message + msg += "\n" + info.Stack.Trace(e.Diag(), "\t", info.Node) + } else { msg = "no details available" } + + // Now simply output the error with the message plus stack trace. args = append(args, msg) - // TODO: ideally we would also have a stack trace to show here. e.Diag().Errorf(err, args...) } // pushScope pushes a new local and context scope. The frame argument indicates whether this is an activation frame, // meaning that searches for local variables will not probe into parent scopes (since they are inaccessible). -func (e *evaluator) pushScope(frame bool) { - e.locals.Push(frame) // pushing the local scope also updates the context scope. +func (e *evaluator) pushScope(frame *rt.StackFrame) { + if frame != nil { + frame.Parent = e.stack // remember the parent so we can pop. + e.stack = frame // install this as the current frame. + } + e.locals.Push(frame != nil) // pushing the local scope also updates the context scope. } // popScope pops the current local and context scopes. -func (e *evaluator) popScope() { +func (e *evaluator) popScope(frame bool) { + if frame { + contract.Assert(e.stack != nil) + e.stack = e.stack.Parent + } e.locals.Pop() // popping the local scope also updates the context scope. } @@ -340,8 +351,8 @@ func (e *evaluator) evalCall(node diag.Diagable, fnc symbols.Function, defer func() { e.fnc = prior }() // Set up a new lexical scope "activation frame" in which we can bind the parameters; restore it upon exit. - e.pushScope(true) - defer e.popScope() + e.pushScope(&rt.StackFrame{Func: fnc, Caller: node}) + defer e.popScope(true) // Invoke the hooks if available. if e.hooks != nil { @@ -445,8 +456,8 @@ func (e *evaluator) evalStatement(node ast.Statement) *Unwind { func (e *evaluator) evalBlock(node *ast.Block) *Unwind { // Push a scope at the start, and pop it at afterwards; both for the symbol context and local variable values. - e.pushScope(false) - defer e.popScope() + e.pushScope(nil) + defer e.popScope(false) for _, stmt := range node.Statements { if uw := e.evalStatement(stmt); uw != nil { @@ -485,10 +496,10 @@ func (e *evaluator) evalTryCatchFinally(node *ast.TryCatchFinally) *Unwind { if types.CanConvert(thrown.Type(), exty) { // This type matched, so this handler will catch the exception. Set the exception variable, // evaluate the block, and swap the Unwind information (thereby "handling" the in-flight exception). - e.pushScope(false) + e.pushScope(nil) e.locals.SetValue(ex, thrown) uw = e.evalBlock(catch.Block) - e.popScope() + e.popScope(false) break } } @@ -719,7 +730,7 @@ func (e *evaluator) evalArrayLiteral(node *ast.ArrayLiteral) (*rt.Object, *Unwin sz := int(sze.NumberValue()) if sz < 0 { // If the size is less than zero, raise a new error. - return nil, NewThrowUnwind(e.NewNegativeArrayLengthException()) + return nil, NewThrowUnwind(e.NewNegativeArrayLengthException(*node.Size)) } arr = make([]rt.Value, sz) } @@ -733,7 +744,8 @@ func (e *evaluator) evalArrayLiteral(node *ast.ArrayLiteral) (*rt.Object, *Unwin arr = make([]rt.Value, 0, len(*node.Elements)) } else if len(*node.Elements) > *sz { // The element count exceeds the size; raise an error. - return nil, NewThrowUnwind(e.NewIncorrectArrayElementCountException(*sz, len(*node.Elements))) + return nil, NewThrowUnwind( + e.NewIncorrectArrayElementCountException(node, *sz, len(*node.Elements))) } for i, elem := range *node.Elements { @@ -901,7 +913,7 @@ func (e *evaluator) evalLoadLocation(node *ast.LoadLocationExpression, lval bool func (e *evaluator) checkThis(node diag.Diagable, this *rt.Object) *Unwind { contract.Assert(this != nil) // binder should catch cases where this isn't true if this.Type() == types.Null { - return NewThrowUnwind(e.NewNullObjectException()) + return NewThrowUnwind(e.NewNullObjectException(node)) } return nil } diff --git a/pkg/eval/exceptions.go b/pkg/eval/exceptions.go index 899a8173e..59d6e629e 100644 --- a/pkg/eval/exceptions.go +++ b/pkg/eval/exceptions.go @@ -3,17 +3,25 @@ package eval import ( + "github.com/marapongo/mu/pkg/diag" "github.com/marapongo/mu/pkg/eval/rt" + "github.com/marapongo/mu/pkg/util/contract" ) -func (e *evaluator) NewNullObjectException() *rt.Object { - return e.alloc.NewException("Target object is null") +// NewException produces a new exception in the evaluator using the current location and stack frames. +func (e *evaluator) NewException(node diag.Diagable, msg string, args ...interface{}) *rt.Object { + contract.Require(node != nil, "node") + return e.alloc.NewException(node, e.stack, msg, args...) } -func (e *evaluator) NewNegativeArrayLengthException() *rt.Object { - return e.alloc.NewException("Invalid array size (must be >= 0)") +func (e *evaluator) NewNullObjectException(node diag.Diagable) *rt.Object { + return e.NewException(node, "Target object is null") } -func (e *evaluator) NewIncorrectArrayElementCountException(expect int, got int) *rt.Object { - return e.alloc.NewException("Invalid number of array elements; expected <=%v, got %v", expect, got) +func (e *evaluator) NewNegativeArrayLengthException(node diag.Diagable) *rt.Object { + return e.NewException(node, "Invalid array size (must be >= 0)") +} + +func (e *evaluator) NewIncorrectArrayElementCountException(node diag.Diagable, expect int, got int) *rt.Object { + return e.NewException(node, "Invalid number of array elements; expected <=%v, got %v", expect, got) } diff --git a/pkg/eval/rt/object.go b/pkg/eval/rt/object.go index 6c24a2003..44b5bfe20 100644 --- a/pkg/eval/rt/object.go +++ b/pkg/eval/rt/object.go @@ -8,6 +8,7 @@ import ( "github.com/marapongo/mu/pkg/compiler/symbols" "github.com/marapongo/mu/pkg/compiler/types" + "github.com/marapongo/mu/pkg/diag" "github.com/marapongo/mu/pkg/tokens" "github.com/marapongo/mu/pkg/util/contract" ) @@ -65,7 +66,7 @@ func (o *Object) StringValue() string { return s } -// FunctionValue asserts that the target is a reference and returns its value. +// FunctionValue asserts that the target is a function and returns its value. func (o *Object) FunctionValue() FuncStub { contract.Assertf(o.value != nil, "Expected Function object to carry a Value; got nil") r, ok := o.value.(FuncStub) @@ -73,7 +74,7 @@ func (o *Object) FunctionValue() FuncStub { return r } -// PointerValue asserts that the target is a reference and returns its value. +// PointerValue asserts that the target is a pointer and returns its value. func (o *Object) PointerValue() *Pointer { contract.Assertf(o.value != nil, "Expected Pointer object to carry a Value; got nil") r, ok := o.value.(*Pointer) @@ -81,13 +82,12 @@ func (o *Object) PointerValue() *Pointer { return r } -// ExceptionMessage asserts that the target is an exception and returns its message. -func (o *Object) ExceptionMessage() string { - contract.Assertf(o.t == types.Exception, "Expected object type to be Exception; got %v", o.t) +// ExceptionValue asserts that the target is an exception and returns its value. +func (o *Object) ExceptionValue() ExceptionInfo { contract.Assertf(o.value != nil, "Expected Exception object to carry a Value; got nil") - s, ok := o.value.(string) - contract.Assertf(ok, "Expected Exception object's Value to be string") - return s + r, ok := o.value.(ExceptionInfo) + contract.Assertf(ok, "Expected Exception object's Value to be an ExceptionInfo") + return r } // GetPropertyAddr returns the reference to an object's property, lazily initializing if 'init' is true, or @@ -216,6 +216,12 @@ func NewFunctionObject(fnc symbols.Function, this *Object) *Object { return NewObject(fnc.FuncType(), stub, nil) } +// FuncStub is a stub that captures a symbol plus an optional instance 'this' object. +type FuncStub struct { + Func symbols.Function + This *Object +} + // NewPointerObject allocates a new pointer-like object that wraps the given reference. func NewPointerObject(t symbols.Type, ptr *Pointer) *Object { contract.Require(ptr != nil, "ptr") @@ -224,9 +230,22 @@ func NewPointerObject(t symbols.Type, ptr *Pointer) *Object { } // NewExceptionObject creates a new exception with the given message. -func NewExceptionObject(message string, args ...interface{}) *Object { - // TODO: capture a stack trace. - return NewPrimitiveObject(types.Exception, fmt.Sprintf(message, args...)) +func NewExceptionObject(node diag.Diagable, stack *StackFrame, message string, args ...interface{}) *Object { + contract.Require(node != nil, "node") + contract.Require(stack != nil, "stack") + info := ExceptionInfo{ + Node: node, + Stack: stack, + Message: fmt.Sprintf(message, args...), + } + return NewPrimitiveObject(types.Exception, info) +} + +// ExceptionInfo captures information about a thrown exception (source, stack, and message). +type ExceptionInfo struct { + Node diag.Diagable // the location that the throw occurred. + Stack *StackFrame // the full linked stack trace. + Message string // the optional pre-formatted error message. } // NewConstantObject returns a new object with the right type and value, based on some constant data. @@ -247,9 +266,3 @@ func NewConstantObject(v interface{}) *Object { return nil } } - -// FuncStub is a stub that captures a symbol plus an optional instance 'this' object. -type FuncStub struct { - Func symbols.Function - This *Object -} diff --git a/pkg/eval/rt/stack.go b/pkg/eval/rt/stack.go new file mode 100644 index 000000000..bbdc051ff --- /dev/null +++ b/pkg/eval/rt/stack.go @@ -0,0 +1,54 @@ +// Copyright 2016 Marapongo, Inc. All rights reserved. + +package rt + +import ( + "bytes" + + "github.com/marapongo/mu/pkg/compiler/symbols" + "github.com/marapongo/mu/pkg/diag" +) + +// StackFrame is a structure that helps us build up a stack trace upon failure. +type StackFrame struct { + Parent *StackFrame // the parent frame. + Func symbols.Function // the current function. + Caller diag.Diagable // the location inside of our caller. +} + +// Trace creates a stack trace from the given stack. If a current location is given, that will be used for the location +// of the first frame; if it is missing, no location will be given. +func (s *StackFrame) Trace(d diag.Sink, prefix string, current diag.Diagable) string { + var trace bytes.Buffer + + for s != nil { + // First print the prefix (tab, spaces, whatever). + trace.WriteString(prefix) + + // Now produce a string indicating the name and signature of the function; this will look like this: + // at package:module:function(A, .., Z)R + // where A are the argument types (if any) and R is the return type (if any). + trace.WriteString("at ") + trace.WriteString(string(s.Func.Token())) + trace.WriteString(string(s.Func.FuncType().Token())) + + // Next, if there's source information about the current location inside of this function, print it. + if current != nil { + if doc, loc := current.Where(); doc != nil || loc != nil { + trace.WriteString(" in ") + trace.WriteString(d.StringifyLocation(doc, loc)) + } + } + + // Remember the current frame's caller position as our next frame's current position. + current = s.Caller + + // Now advance to the parent (or break out if we have reached the top). + s = s.Parent + if s != nil { + trace.WriteString("\n") + } + } + + return trace.String() +}