From cc4a4c755f51496eec2286439a12386136b576ef Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sat, 28 Mar 2020 20:46:09 +0100 Subject: [PATCH] interp: show backtrace with error This should make it much easier to figure out why and where an error happens at package initialization time. --- compiler/compiler.go | 3 ++- interp/errors.go | 49 +++++++++++++++---------------------- interp/frame.go | 58 ++++++++++++++++++++++++-------------------- interp/interp.go | 2 +- interp/scan.go | 5 ++-- main.go | 26 ++++++++++---------- 6 files changed, 70 insertions(+), 73 deletions(-) diff --git a/compiler/compiler.go b/compiler/compiler.go index a40c5b48..33b5ab4f 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -887,7 +887,8 @@ func (b *builder) createFunctionDefinition() { if b.fn.Synthetic == "package initializer" { // Package initializers have no debug info. Create some fake debug // info to at least have *something*. - b.difunc = b.attachDebugInfoRaw(b.fn, b.fn.LLVMFn, "", "", 0) + filename := b.fn.Package().Pkg.Path() + "/" + b.difunc = b.attachDebugInfoRaw(b.fn, b.fn.LLVMFn, "", filename, 0) } else if b.fn.Syntax() != nil { // Create debug info file if needed. b.difunc = b.attachDebugInfo(b.fn) diff --git a/interp/errors.go b/interp/errors.go index 26b3ea37..f8c7e4e8 100644 --- a/interp/errors.go +++ b/interp/errors.go @@ -13,55 +13,44 @@ import ( // errUnreachable is returned when an unreachable instruction is executed. This // error should not be visible outside of the interp package. -var errUnreachable = errors.New("interp: unreachable executed") - -// Unsupported is the specific error that is returned when an unsupported -// instruction is hit while trying to interpret all initializers. -type Unsupported struct { - ImportPath string - Inst llvm.Value - Pos token.Position -} - -func (e Unsupported) Error() string { - // TODO: how to return the actual instruction string? - // It looks like LLVM provides no function for that... - return scanner.Error{ - Pos: e.Pos, - Msg: "interp: unsupported instruction", - }.Error() -} +var errUnreachable = &Error{Err: errors.New("interp: unreachable executed")} // unsupportedInstructionError returns a new "unsupported instruction" error for // the given instruction. It includes source location information, when // available. -func (e *evalPackage) unsupportedInstructionError(inst llvm.Value) *Unsupported { - return &Unsupported{ - ImportPath: e.packagePath, - Inst: inst, - Pos: getPosition(inst), - } +func (e *evalPackage) unsupportedInstructionError(inst llvm.Value) *Error { + return e.errorAt(inst, errors.New("interp: unsupported instruction")) +} + +// ErrorLine is one line in a traceback. The position may be missing. +type ErrorLine struct { + Pos token.Position + Inst llvm.Value } // Error encapsulates compile-time interpretation errors with an associated // import path. The errors may not have a precise location attached. type Error struct { ImportPath string - Errs []scanner.Error + Inst llvm.Value + Pos token.Position + Err error + Traceback []ErrorLine } // Error returns the string of the first error in the list of errors. -func (e Error) Error() string { - return e.Errs[0].Error() +func (e *Error) Error() string { + return e.Pos.String() + ": " + e.Err.Error() } // errorAt returns an error value for the currently interpreted package at the // location of the instruction. The location information may not be complete as // it depends on debug information in the IR. -func (e *evalPackage) errorAt(inst llvm.Value, msg string) Error { - return Error{ +func (e *evalPackage) errorAt(inst llvm.Value, err error) *Error { + return &Error{ ImportPath: e.packagePath, - Errs: []scanner.Error{errorAt(inst, msg)}, + Pos: getPosition(inst), + Err: err, } } diff --git a/interp/frame.go b/interp/frame.go index 299a1778..ed7c3f98 100644 --- a/interp/frame.go +++ b/interp/frame.go @@ -4,6 +4,7 @@ package interp // functions. import ( + "errors" "strings" "tinygo.org/x/go-llvm" @@ -21,7 +22,7 @@ type frame struct { // Most of it works at compile time. Some calls get translated into calls to be // executed at runtime: calls to functions with side effects, external calls, // and operations on the result of such instructions. -func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (retval Value, outgoing []llvm.Value, err error) { +func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (retval Value, outgoing []llvm.Value, err *Error) { for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) { if fr.Debug { print(indent) @@ -97,7 +98,7 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re value = operand.Load() } if value.Type() != inst.Type() { - return nil, nil, fr.errorAt(inst, "interp: load: type does not match") + return nil, nil, fr.errorAt(inst, errors.New("interp: load: type does not match")) } fr.locals[inst] = fr.getValue(value) case !inst.IsAStoreInst().IsNil(): @@ -121,16 +122,16 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re // Not a constant operation. // This should be detected by the scanner, but isn't at the // moment. - return nil, nil, fr.errorAt(inst, "todo: non-const gep") + return nil, nil, fr.errorAt(inst, errors.New("todo: non-const gep")) } indices[i] = uint32(operand.Value().ZExtValue()) } result, err := value.GetElementPtr(indices) if err != nil { - return nil, nil, fr.errorAt(inst, err.Error()) + return nil, nil, fr.errorAt(inst, err) } if result.Type() != inst.Type() { - return nil, nil, fr.errorAt(inst, "interp: gep: type does not match") + return nil, nil, fr.errorAt(inst, errors.New("interp: gep: type does not match")) } fr.locals[inst] = result @@ -185,7 +186,7 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re } } // It is not possible in Go to bitcast a map value to a pointer. - return nil, nil, fr.errorAt(inst, "unimplemented: bitcast of map") + return nil, nil, fr.errorAt(inst, errors.New("unimplemented: bitcast of map")) } value := fr.getLocal(operand).(*LocalValue) fr.locals[inst] = &LocalValue{fr.Eval, fr.builder.CreateBitCast(value.Value(), inst.Type(), "")} @@ -269,7 +270,7 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re } else { result, err := result.GetElementPtr([]uint32{0, 0}) if err != nil { - return nil, nil, errorAt(inst, err.Error()) + return nil, nil, fr.errorAt(inst, err) } fr.locals[resultInst] = result } @@ -374,30 +375,30 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re // Re-add this GEP, in the hope that it it is then of the correct type... dstArrayValue, err := dstArray.GetElementPtr([]uint32{0, 0}) if err != nil { - return nil, nil, errorAt(inst, err.Error()) + return nil, nil, fr.errorAt(inst, err) } dstArray = dstArrayValue.(*LocalValue) srcArrayValue, err := srcArray.GetElementPtr([]uint32{0, 0}) if err != nil { - return nil, nil, errorAt(inst, err.Error()) + return nil, nil, fr.errorAt(inst, err) } srcArray = srcArrayValue.(*LocalValue) } if fr.Eval.TargetData.TypeAllocSize(dstArray.Type().ElementType()) != elementSize { - return nil, nil, fr.errorAt(inst, "interp: slice dst element size does not match pointer type") + return nil, nil, fr.errorAt(inst, errors.New("interp: slice dst element size does not match pointer type")) } if fr.Eval.TargetData.TypeAllocSize(srcArray.Type().ElementType()) != elementSize { - return nil, nil, fr.errorAt(inst, "interp: slice src element size does not match pointer type") + return nil, nil, fr.errorAt(inst, errors.New("interp: slice src element size does not match pointer type")) } if dstArray.Type() != srcArray.Type() { - return nil, nil, fr.errorAt(inst, "interp: slice element types don't match") + return nil, nil, fr.errorAt(inst, errors.New("interp: slice element types don't match")) } length := dstLen.Value().SExtValue() if srcLength := srcLen.Value().SExtValue(); srcLength < length { length = srcLength } if length < 0 { - return nil, nil, fr.errorAt(inst, "interp: trying to copy a slice with negative length?") + return nil, nil, fr.errorAt(inst, errors.New("interp: trying to copy a slice with negative length?")) } for i := int64(0); i < length; i++ { var err error @@ -406,13 +407,13 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re // dst++ dstArrayValue, err := dstArray.GetElementPtr([]uint32{1}) if err != nil { - return nil, nil, errorAt(inst, err.Error()) + return nil, nil, fr.errorAt(inst, err) } dstArray = dstArrayValue.(*LocalValue) // src++ srcArrayValue, err := srcArray.GetElementPtr([]uint32{1}) if err != nil { - return nil, nil, errorAt(inst, err.Error()) + return nil, nil, fr.errorAt(inst, err) } srcArray = srcArrayValue.(*LocalValue) } @@ -444,11 +445,11 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re actualTypeInt := fr.getLocal(inst.Operand(0)).(*LocalValue).Underlying assertedType := fr.getLocal(inst.Operand(1)).(*LocalValue).Underlying if actualTypeInt.IsAConstantExpr().IsNil() || actualTypeInt.Opcode() != llvm.PtrToInt { - return nil, nil, fr.errorAt(inst, "interp: expected typecode in runtime.typeAssert to be a ptrtoint") + return nil, nil, fr.errorAt(inst, errors.New("interp: expected typecode in runtime.typeAssert to be a ptrtoint")) } actualType := actualTypeInt.Operand(0) if actualType.IsAConstant().IsNil() || assertedType.IsAConstant().IsNil() { - return nil, nil, fr.errorAt(inst, "interp: unimplemented: type assert with non-constant interface value") + return nil, nil, fr.errorAt(inst, errors.New("interp: unimplemented: type assert with non-constant interface value")) } assertOk := uint64(0) if llvm.ConstExtractValue(actualType.Initializer(), []uint32{0}) == assertedType { @@ -459,16 +460,16 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re typecode := fr.getLocal(inst.Operand(0)).(*LocalValue).Underlying interfaceMethodSet := fr.getLocal(inst.Operand(1)).(*LocalValue).Underlying if typecode.IsAConstantExpr().IsNil() || typecode.Opcode() != llvm.PtrToInt { - return nil, nil, fr.errorAt(inst, "interp: expected typecode to be a ptrtoint") + return nil, nil, fr.errorAt(inst, errors.New("interp: expected typecode to be a ptrtoint")) } typecode = typecode.Operand(0) if interfaceMethodSet.IsAConstantExpr().IsNil() || interfaceMethodSet.Opcode() != llvm.GetElementPtr { - return nil, nil, fr.errorAt(inst, "interp: expected method set in runtime.interfaceImplements to be a constant gep") + return nil, nil, fr.errorAt(inst, errors.New("interp: expected method set in runtime.interfaceImplements to be a constant gep")) } interfaceMethodSet = interfaceMethodSet.Operand(0).Initializer() methodSet := llvm.ConstExtractValue(typecode.Initializer(), []uint32{1}) if methodSet.IsAConstantExpr().IsNil() || methodSet.Opcode() != llvm.GetElementPtr { - return nil, nil, fr.errorAt(inst, "interp: expected method set to be a constant gep") + return nil, nil, fr.errorAt(inst, errors.New("interp: expected method set to be a constant gep")) } methodSet = methodSet.Operand(0).Initializer() @@ -568,6 +569,11 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re // interpret anyway and hope for the best. ret, err = fr.function(callee, params, indent+" ") if err != nil { + // Record this function call in the backtrace. + err.Traceback = append(err.Traceback, ErrorLine{ + Pos: getPosition(inst), + Inst: inst, + }) return nil, nil, err } } @@ -586,7 +592,7 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re fr.locals[inst] = fr.getValue(newValue) } else { if len(indices) != 1 { - return nil, nil, fr.errorAt(inst, "interp: cannot handle extractvalue with not exactly 1 index") + return nil, nil, fr.errorAt(inst, errors.New("interp: cannot handle extractvalue with not exactly 1 index")) } fr.locals[inst] = &LocalValue{fr.Eval, fr.builder.CreateExtractValue(agg.Underlying, int(indices[0]), inst.Name())} } @@ -599,7 +605,7 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re fr.locals[inst] = &LocalValue{fr.Eval, newValue} } else { if len(indices) != 1 { - return nil, nil, fr.errorAt(inst, "interp: cannot handle insertvalue with not exactly 1 index") + return nil, nil, fr.errorAt(inst, errors.New("interp: cannot handle insertvalue with not exactly 1 index")) } fr.locals[inst] = &LocalValue{fr.Eval, fr.builder.CreateInsertValue(agg.Underlying, val.Value(), int(indices[0]), inst.Name())} } @@ -624,17 +630,17 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re // conditional branch (if/then/else) cond := fr.getLocal(inst.Operand(0)).Value() if cond.Type() != fr.Mod.Context().Int1Type() { - return nil, nil, fr.errorAt(inst, "expected an i1 in a branch instruction") + return nil, nil, fr.errorAt(inst, errors.New("expected an i1 in a branch instruction")) } thenBB := inst.Operand(1) elseBB := inst.Operand(2) if !cond.IsAInstruction().IsNil() { - return nil, nil, fr.errorAt(inst, "interp: branch on a non-constant") + return nil, nil, fr.errorAt(inst, errors.New("interp: branch on a non-constant")) } if !cond.IsAConstantExpr().IsNil() { // This may happen when the instruction builder could not // const-fold some instructions. - return nil, nil, fr.errorAt(inst, "interp: branch on a non-const-propagated constant expression") + return nil, nil, fr.errorAt(inst, errors.New("interp: branch on a non-const-propagated constant expression")) } switch cond { case llvm.ConstInt(fr.Mod.Context().Int1Type(), 0, false): // false @@ -642,7 +648,7 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re case llvm.ConstInt(fr.Mod.Context().Int1Type(), 1, false): // true return nil, []llvm.Value{elseBB}, nil // else default: - return nil, nil, fr.errorAt(inst, "branch was not true or false") + return nil, nil, fr.errorAt(inst, errors.New("branch was not true or false")) } case !inst.IsABranchInst().IsNil() && inst.OperandsCount() == 1: // unconditional branch (goto) diff --git a/interp/interp.go b/interp/interp.go index cea2316f..d5ff3965 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -97,7 +97,7 @@ func Run(mod llvm.Module, debug bool) error { // function interprets the given function. The params are the function params // and the indent is the string indentation to use when dumping all interpreted // instructions. -func (e *evalPackage) function(fn llvm.Value, params []Value, indent string) (Value, error) { +func (e *evalPackage) function(fn llvm.Value, params []Value, indent string) (Value, *Error) { fr := frame{ evalPackage: e, fn: fn, diff --git a/interp/scan.go b/interp/scan.go index 65306562..adeb8f43 100644 --- a/interp/scan.go +++ b/interp/scan.go @@ -1,6 +1,7 @@ package interp import ( + "errors" "strings" "tinygo.org/x/go-llvm" @@ -40,7 +41,7 @@ type sideEffectResult struct { // hasSideEffects scans this function and all descendants, recursively. It // returns whether this function has side effects and if it does, which globals // it mentions anywhere in this function or any called functions. -func (e *evalPackage) hasSideEffects(fn llvm.Value) (*sideEffectResult, error) { +func (e *evalPackage) hasSideEffects(fn llvm.Value) (*sideEffectResult, *Error) { name := fn.Name() switch { case name == "runtime.alloc": @@ -99,7 +100,7 @@ func (e *evalPackage) hasSideEffects(fn llvm.Value) (*sideEffectResult, error) { switch inst.InstructionOpcode() { case llvm.IndirectBr, llvm.Invoke: // Not emitted by the compiler. - return nil, e.errorAt(inst, "unknown instructions") + return nil, e.errorAt(inst, errors.New("unknown instructions")) case llvm.Call: child := inst.CalledValue() if !child.IsAInlineAsm().IsNil() { diff --git a/main.go b/main.go index d4361d13..62856a52 100644 --- a/main.go +++ b/main.go @@ -658,22 +658,22 @@ func usage() { // to limitations in the LLVM bindings. func printCompilerError(logln func(...interface{}), err error) { switch err := err.(type) { - case *interp.Unsupported: - // hit an unknown/unsupported instruction - logln("#", err.ImportPath) - msg := "unsupported instruction during init evaluation:" - if err.Pos.String() != "" { - msg = err.Pos.String() + " " + msg - } - logln(msg) - err.Inst.Dump() - logln() case types.Error, scanner.Error: logln(err) - case interp.Error: + case *interp.Error: logln("#", err.ImportPath) - for _, err := range err.Errs { - logln(err) + logln(err.Error()) + if !err.Inst.IsNil() { + err.Inst.Dump() + logln() + } + if len(err.Traceback) > 0 { + logln("\ntraceback:") + for _, line := range err.Traceback { + logln(line.Pos.String() + ":") + line.Inst.Dump() + logln() + } } case loader.Errors: logln("#", err.Pkg.ImportPath)