diff --git a/go.mod b/go.mod index 25a22892..810ebeed 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,5 @@ require ( go.bug.st/serial.v1 v0.0.0-20180827123349-5f7892a7bb45 golang.org/x/sys v0.0.0-20191010194322-b09406accb47 // indirect golang.org/x/tools v0.0.0-20190227180812-8dcc6e70cdef - tinygo.org/x/go-llvm v0.0.0-20191113125529-bad6d01809e8 + tinygo.org/x/go-llvm v0.0.0-20191124211856-b2db3df3f257 ) diff --git a/go.sum b/go.sum index 1cd24e10..6009c588 100644 --- a/go.sum +++ b/go.sum @@ -28,3 +28,5 @@ tinygo.org/x/go-llvm v0.0.0-20191103200204-37e93e3f04e2 h1:Q5Hv3e5cLMGkiYwYgZL1Z tinygo.org/x/go-llvm v0.0.0-20191103200204-37e93e3f04e2/go.mod h1:fv1F0BSNpxMfCL0zF3M4OPFbgYHnhtB6ST0HvUtu/LE= tinygo.org/x/go-llvm v0.0.0-20191113125529-bad6d01809e8 h1:9Bfvso+tTVQg16UzOA614NaYA4x8vsRBNtd3eBrXwp0= tinygo.org/x/go-llvm v0.0.0-20191113125529-bad6d01809e8/go.mod h1:fv1F0BSNpxMfCL0zF3M4OPFbgYHnhtB6ST0HvUtu/LE= +tinygo.org/x/go-llvm v0.0.0-20191124211856-b2db3df3f257 h1:o8VDylrMN7gWemBMu8rEyuogKPhcLTdx5KrUAp9macc= +tinygo.org/x/go-llvm v0.0.0-20191124211856-b2db3df3f257/go.mod h1:fv1F0BSNpxMfCL0zF3M4OPFbgYHnhtB6ST0HvUtu/LE= diff --git a/interp/errors.go b/interp/errors.go index 4fb9159a..c3cf0a1a 100644 --- a/interp/errors.go +++ b/interp/errors.go @@ -3,15 +3,89 @@ package interp // This file provides useful types for errors encountered during IR evaluation. import ( + "errors" + "go/scanner" + "go/token" + "path/filepath" + "tinygo.org/x/go-llvm" ) +// 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 { - Inst llvm.Value + 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 "interp: unsupported instruction" + return scanner.Error{ + Pos: e.Pos, + Msg: "interp: unsupported instruction", + }.Error() +} + +// 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), + } +} + +// 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 +} + +// Error returns the string of the first error in the list of errors. +func (e Error) Error() string { + return e.Errs[0].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{ + ImportPath: e.packagePath, + Errs: []scanner.Error{errorAt(inst, msg)}, + } +} + +// errorAt returns an error value at the location of the instruction. +// The location information may not be complete as it depends on debug +// information in the IR. +func errorAt(inst llvm.Value, msg string) scanner.Error { + return scanner.Error{ + Pos: getPosition(inst), + Msg: msg, + } +} + +// getPosition returns the position information for the given instruction, as +// far as it is available. +func getPosition(inst llvm.Value) token.Position { + loc := inst.InstructionDebugLoc() + if loc.IsNil() { + return token.Position{} + } + file := loc.LocationScope().ScopeFile() + return token.Position{ + Filename: filepath.Join(file.FileDirectory(), file.FileFilename()), + Line: int(loc.LocationLine()), + Column: int(loc.LocationColumn()), + } } diff --git a/interp/frame.go b/interp/frame.go index adb19cce..6f249d96 100644 --- a/interp/frame.go +++ b/interp/frame.go @@ -4,21 +4,17 @@ package interp // functions. import ( - "errors" "strings" "tinygo.org/x/go-llvm" ) type frame struct { - *Eval - fn llvm.Value - pkgName string - locals map[llvm.Value]Value + *evalPackage + fn llvm.Value + locals map[llvm.Value]Value } -var ErrUnreachable = errors.New("interp: unreachable executed") - // evalBasicBlock evaluates a single basic block, returning the return value (if // ending with a ret instruction), a list of outgoing basic blocks (if not // ending with a ret instruction), or an error on failure. @@ -79,13 +75,13 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re fr.locals[inst] = &LocalValue{fr.Eval, fr.builder.CreateXor(lhs, rhs, "")} default: - return nil, nil, &Unsupported{inst} + return nil, nil, fr.unsupportedInstructionError(inst) } // Memory operators case !inst.IsAAllocaInst().IsNil(): allocType := inst.Type().ElementType() - alloca := llvm.AddGlobal(fr.Mod, allocType, fr.pkgName+"$alloca") + alloca := llvm.AddGlobal(fr.Mod, allocType, fr.packagePath+"$alloca") alloca.SetInitializer(llvm.ConstNull(allocType)) alloca.SetLinkage(llvm.InternalLinkage) fr.locals[inst] = &LocalValue{ @@ -247,12 +243,12 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re if size != typeSize { // allocate an array if size%typeSize != 0 { - return nil, nil, &Unsupported{inst} + return nil, nil, fr.unsupportedInstructionError(inst) } elementCount = int(size / typeSize) allocType = llvm.ArrayType(allocType, elementCount) } - alloc := llvm.AddGlobal(fr.Mod, allocType, fr.pkgName+"$alloc") + alloc := llvm.AddGlobal(fr.Mod, allocType, fr.packagePath+"$alloc") alloc.SetInitializer(llvm.ConstNull(allocType)) alloc.SetLinkage(llvm.InternalLinkage) result := &LocalValue{ @@ -270,13 +266,16 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re valueSize := inst.Operand(1).ZExtValue() fr.locals[inst] = &MapValue{ Eval: fr.Eval, - PkgName: fr.pkgName, + PkgName: fr.packagePath, KeySize: int(keySize), ValueSize: int(valueSize), } case callee.Name() == "runtime.hashmapStringSet": // set a string key in the map - m := fr.getLocal(inst.Operand(0)).(*MapValue) + m, ok := fr.getLocal(inst.Operand(0)).(*MapValue) + if !ok { + return nil, nil, fr.errorAt(inst, "could not update map with string key") + } // "key" is a Go string value, which in the TinyGo calling convention is split up // into separate pointer and length parameters. keyBuf := fr.getLocal(inst.Operand(1)).(*LocalValue) @@ -285,7 +284,10 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re m.PutString(keyBuf, keyLen, valPtr) case callee.Name() == "runtime.hashmapBinarySet": // set a binary (int etc.) key in the map - m := fr.getLocal(inst.Operand(0)).(*MapValue) + m, ok := fr.getLocal(inst.Operand(0)).(*MapValue) + if !ok { + return nil, nil, fr.errorAt(inst, "could not update map") + } keyBuf := fr.getLocal(inst.Operand(1)).(*LocalValue) valPtr := fr.getLocal(inst.Operand(2)).(*LocalValue) m.PutBinary(keyBuf, valPtr) @@ -304,7 +306,7 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re } globalType := llvm.ArrayType(fr.Mod.Context().Int8Type(), len(result)) globalValue := llvm.ConstArray(fr.Mod.Context().Int8Type(), vals) - global := llvm.AddGlobal(fr.Mod, globalType, fr.pkgName+"$stringconcat") + global := llvm.AddGlobal(fr.Mod, globalType, fr.packagePath+"$stringconcat") global.SetInitializer(globalValue) global.SetLinkage(llvm.InternalLinkage) global.SetGlobalConstant(true) @@ -336,20 +338,20 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re srcArray = srcArray.GetElementPtr([]uint32{0, 0}).(*LocalValue) } if fr.Eval.TargetData.TypeAllocSize(dstArray.Type().ElementType()) != elementSize { - return nil, nil, errors.New("interp: slice dst element size does not match pointer type") + return nil, nil, fr.errorAt(inst, "interp: slice dst element size does not match pointer type") } if fr.Eval.TargetData.TypeAllocSize(srcArray.Type().ElementType()) != elementSize { - return nil, nil, errors.New("interp: slice src element size does not match pointer type") + return nil, nil, fr.errorAt(inst, "interp: slice src element size does not match pointer type") } if dstArray.Type() != srcArray.Type() { - return nil, nil, errors.New("interp: slice element types don't match") + return nil, nil, fr.errorAt(inst, "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, errors.New("interp: trying to copy a slice with negative length?") + return nil, nil, fr.errorAt(inst, "interp: trying to copy a slice with negative length?") } for i := int64(0); i < length; i++ { // *dst = *src @@ -370,7 +372,7 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re } globalType := llvm.ArrayType(fr.Mod.Context().Int8Type(), len(result)) globalValue := llvm.ConstArray(fr.Mod.Context().Int8Type(), vals) - global := llvm.AddGlobal(fr.Mod, globalType, fr.pkgName+"$bytes") + global := llvm.AddGlobal(fr.Mod, globalType, fr.packagePath+"$bytes") global.SetInitializer(globalValue) global.SetLinkage(llvm.InternalLinkage) global.SetGlobalConstant(true) @@ -489,7 +491,7 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re // compile time. // * Unbounded: cannot call at runtime so we'll try to // interpret anyway and hope for the best. - ret, err = fr.function(callee, params, fr.pkgName, indent+" ") + ret, err = fr.function(callee, params, indent+" ") if err != nil { return nil, nil, err } @@ -499,7 +501,7 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re } default: // function pointers, etc. - return nil, nil, &Unsupported{inst} + return nil, nil, fr.unsupportedInstructionError(inst) } case !inst.IsAExtractValueInst().IsNil(): agg := fr.getLocal(inst.Operand(0)).(*LocalValue) // must be constant @@ -509,7 +511,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, errors.New("cannot handle extractvalue with not exactly 1 index") + return nil, nil, fr.errorAt(inst, "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())} } @@ -522,7 +524,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, errors.New("cannot handle insertvalue with not exactly 1 index") + return nil, nil, fr.errorAt(inst, "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())} } @@ -540,12 +542,12 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re thenBB := inst.Operand(1) elseBB := inst.Operand(2) if !cond.IsAInstruction().IsNil() { - return nil, nil, errors.New("interp: branch on a non-constant") + return nil, nil, fr.errorAt(inst, "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, errors.New("interp: branch on a non-const-propagated constant expression") + return nil, nil, fr.errorAt(inst, "interp: branch on a non-const-propagated constant expression") } switch cond { case llvm.ConstInt(fr.Mod.Context().Int1Type(), 0, false): // false @@ -561,10 +563,11 @@ func (fr *frame) evalBasicBlock(bb, incoming llvm.BasicBlock, indent string) (re case !inst.IsAUnreachableInst().IsNil(): // Unreachable was reached (e.g. after a call to panic()). // Report this as an error, as it is not supposed to happen. - return nil, nil, ErrUnreachable + // This is a sentinel error value. + return nil, nil, errUnreachable default: - return nil, nil, &Unsupported{inst} + return nil, nil, fr.unsupportedInstructionError(inst) } } diff --git a/interp/interp.go b/interp/interp.go index e1005657..f91c31f3 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -7,7 +7,6 @@ package interp // methods. import ( - "errors" "strings" "tinygo.org/x/go-llvm" @@ -22,6 +21,15 @@ type Eval struct { sideEffectFuncs map[llvm.Value]*sideEffectResult // cache of side effect scan results } +// evalPackage encapsulates the Eval type for just a single package. The Eval +// type keeps state across the whole program, the evalPackage type keeps extra +// state for the currently interpreted package. +type evalPackage struct { + *Eval + packagePath string + errors []error +} + // Run evaluates the function with the given name and then eliminates all // callers. func Run(mod llvm.Module, debug bool) error { @@ -56,7 +64,7 @@ func Run(mod llvm.Module, debug bool) error { break // ret void } if inst.IsACallInst().IsNil() || inst.CalledValue().IsAFunction().IsNil() { - return errors.New("expected all instructions in " + name + " to be direct calls") + return errorAt(inst, "interp: expected all instructions in "+name+" to be direct calls") } initCalls = append(initCalls, inst) } @@ -66,13 +74,17 @@ func Run(mod llvm.Module, debug bool) error { for _, call := range initCalls { initName := call.CalledValue().Name() if !strings.HasSuffix(initName, ".init") { - return errors.New("expected all instructions in " + name + " to be *.init() calls") + return errorAt(call, "interp: expected all instructions in "+name+" to be *.init() calls") } pkgName := initName[:len(initName)-5] fn := call.CalledValue() call.EraseFromParentAsInstruction() - _, err := e.Function(fn, []Value{&LocalValue{e, undefPtr}, &LocalValue{e, undefPtr}}, pkgName) - if err == ErrUnreachable { + evalPkg := evalPackage{ + Eval: e, + packagePath: pkgName, + } + _, err := evalPkg.function(fn, []Value{&LocalValue{e, undefPtr}, &LocalValue{e, undefPtr}}, "") + if err == errUnreachable { break } if err != nil { @@ -83,16 +95,14 @@ func Run(mod llvm.Module, debug bool) error { return nil } -func (e *Eval) Function(fn llvm.Value, params []Value, pkgName string) (Value, error) { - return e.function(fn, params, pkgName, "") -} - -func (e *Eval) function(fn llvm.Value, params []Value, pkgName, indent string) (Value, 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) { fr := frame{ - Eval: e, - fn: fn, - pkgName: pkgName, - locals: make(map[llvm.Value]Value), + evalPackage: e, + fn: fn, + locals: make(map[llvm.Value]Value), } for i, param := range fn.Params() { fr.locals[param] = params[i] diff --git a/main.go b/main.go index 0cf05183..7cb1c241 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "errors" "flag" "fmt" + "go/scanner" "go/types" "io" "os" @@ -480,11 +481,21 @@ func handleCompilerError(err error) { switch err := err.(type) { case *interp.Unsupported: // hit an unknown/unsupported instruction - fmt.Fprintln(os.Stderr, "unsupported instruction during init evaluation:") + fmt.Fprintln(os.Stderr, "#", err.ImportPath) + msg := "unsupported instruction during init evaluation:" + if err.Pos.String() != "" { + msg = err.Pos.String() + " " + msg + } + fmt.Fprintln(os.Stderr, msg) err.Inst.Dump() fmt.Fprintln(os.Stderr) - case types.Error: + case types.Error, scanner.Error: fmt.Fprintln(os.Stderr, err) + case interp.Error: + fmt.Fprintln(os.Stderr, "#", err.ImportPath) + for _, err := range err.Errs { + fmt.Fprintln(os.Stderr, err) + } case loader.Errors: fmt.Fprintln(os.Stderr, "#", err.Pkg.ImportPath) for _, err := range err.Errs {