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.
This commit is contained in:
joeduffy 2017-02-12 09:38:19 -08:00
parent a16bb714e4
commit 36b4a6f848
6 changed files with 202 additions and 89 deletions

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

54
pkg/eval/rt/stack.go Normal file
View file

@ -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()
}