diff --git a/builder/build.go b/builder/build.go index dc360b92e..035556c3b 100644 --- a/builder/build.go +++ b/builder/build.go @@ -446,7 +446,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe if pkgInit.IsNil() { panic("init not found for " + pkg.Pkg.Path()) } - err := interp.RunFunc(pkgInit, config.Options.InterpTimeout, config.DumpSSA()) + err := interp.RunFunc(pkgInit, config.Options.InterpTimeout, config.Options.InterpMaxDepth, config.DumpSSA()) if err != nil { return err } @@ -1043,7 +1043,7 @@ func createEmbedObjectFile(data, hexSum, sourceFile, sourceDir, tmpdir string, c // needed to convert a program to its final form. Some transformations are not // optional and must be run as the compiler expects them to run. func optimizeProgram(mod llvm.Module, config *compileopts.Config) error { - err := interp.Run(mod, config.Options.InterpTimeout, config.DumpSSA()) + err := interp.Run(mod, config.Options.InterpTimeout, config.Options.InterpMaxDepth, config.DumpSSA()) if err != nil { return err } diff --git a/compileopts/options.go b/compileopts/options.go index 4440f4cf6..0b748a5f5 100644 --- a/compileopts/options.go +++ b/compileopts/options.go @@ -32,6 +32,7 @@ type Options struct { Serial string Work bool // -work flag to print temporary build directory InterpTimeout time.Duration + InterpMaxDepth int PrintIR bool DumpSSA bool VerifyIR bool diff --git a/interp/errors.go b/interp/errors.go index 7aad39fb8..50689152e 100644 --- a/interp/errors.go +++ b/interp/errors.go @@ -19,6 +19,7 @@ var ( errUnsupportedRuntimeInst = errors.New("interp: unsupported instruction (to be emitted at runtime)") errMapAlreadyCreated = errors.New("interp: map already created") errLoopUnrolled = errors.New("interp: loop unrolled") + errDepthExceeded = errors.New("interp: depth exceeded") ) // This is one of the errors that can be returned from toLLVMValue when the @@ -29,7 +30,7 @@ var errInvalidPtrToIntSize = errors.New("interp: ptrtoint integer size does not func isRecoverableError(err error) bool { return err == errIntegerAsPointer || err == errUnsupportedInst || err == errUnsupportedRuntimeInst || err == errMapAlreadyCreated || - err == errLoopUnrolled + err == errLoopUnrolled || err == errDepthExceeded } // ErrorLine is one line in a traceback. The position may be missing. diff --git a/interp/interp.go b/interp/interp.go index 7833dfe93..23311ff17 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -31,10 +31,11 @@ type runner struct { globals map[llvm.Value]int // map from global to index in objects slice start time.Time timeout time.Duration + maxDepth int callsExecuted uint64 } -func newRunner(mod llvm.Module, timeout time.Duration, debug bool) *runner { +func newRunner(mod llvm.Module, timeout time.Duration, maxDepth int, debug bool) *runner { r := runner{ mod: mod, targetData: llvm.NewTargetData(mod.DataLayout()), @@ -44,6 +45,7 @@ func newRunner(mod llvm.Module, timeout time.Duration, debug bool) *runner { globals: make(map[llvm.Value]int), start: time.Now(), timeout: timeout, + maxDepth: maxDepth, } r.pointerSize = uint32(r.targetData.PointerSize()) r.i8ptrType = llvm.PointerType(mod.Context().Int8Type(), 0) @@ -60,8 +62,8 @@ func (r *runner) dispose() { // Run evaluates runtime.initAll function as much as possible at compile time. // Set debug to true if it should print output while running. -func Run(mod llvm.Module, timeout time.Duration, debug bool) error { - r := newRunner(mod, timeout, debug) +func Run(mod llvm.Module, timeout time.Duration, maxDepth int, debug bool) error { + r := newRunner(mod, timeout, maxDepth, debug) defer r.dispose() initAll := mod.NamedFunction("runtime.initAll") @@ -114,7 +116,7 @@ func Run(mod llvm.Module, timeout time.Duration, debug bool) error { if r.debug { fmt.Fprintln(os.Stderr, "call:", fn.Name()) } - _, mem, callErr := r.run(r.getFunction(fn), nil, nil, " ") + _, mem, callErr := r.run(r.getFunction(fn), nil, nil, 0, " ") call.EraseFromParentAsInstruction() if callErr != nil { if isRecoverableError(callErr.Err) { @@ -201,10 +203,10 @@ func Run(mod llvm.Module, timeout time.Duration, debug bool) error { // RunFunc evaluates a single package initializer at compile time. // Set debug to true if it should print output while running. -func RunFunc(fn llvm.Value, timeout time.Duration, debug bool) error { +func RunFunc(fn llvm.Value, timeout time.Duration, maxDepth int, debug bool) error { // Create and initialize *runner object. mod := fn.GlobalParent() - r := newRunner(mod, timeout, debug) + r := newRunner(mod, timeout, maxDepth, debug) defer r.dispose() initName := fn.Name() if !strings.HasSuffix(initName, ".init") { @@ -235,7 +237,7 @@ func RunFunc(fn llvm.Value, timeout time.Duration, debug bool) error { if r.debug { fmt.Fprintln(os.Stderr, "interp:", fn.Name()) } - _, pkgMem, callErr := r.run(r.getFunction(fn), nil, nil, " ") + _, pkgMem, callErr := r.run(r.getFunction(fn), nil, nil, 0, " ") if callErr != nil { if isRecoverableError(callErr.Err) { // Could not finish, but could recover from it. diff --git a/interp/interp_test.go b/interp/interp_test.go index fc567af20..71776a3ce 100644 --- a/interp/interp_test.go +++ b/interp/interp_test.go @@ -53,7 +53,7 @@ func runTest(t *testing.T, pathPrefix string) { defer mod.Dispose() // Perform the transform. - err = Run(mod, 10*time.Minute, false) + err = Run(mod, 10*time.Minute, 10, false) if err != nil { if err, match := err.(*Error); match { println(err.Error()) diff --git a/interp/interpreter.go b/interp/interpreter.go index dcea0044a..84bbc953a 100644 --- a/interp/interpreter.go +++ b/interp/interpreter.go @@ -12,7 +12,7 @@ import ( "tinygo.org/x/go-llvm" ) -func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent string) (value, memoryView, *Error) { +func (r *runner) run(fn *function, params []value, parentMem *memoryView, depth int, indent string) (value, memoryView, *Error) { mem := memoryView{r: r, parent: parentMem} locals := make([]value, len(fn.locals)) r.callsExecuted++ @@ -35,6 +35,11 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent startRTInsts := len(mem.instructions) for instIndex := 0; instIndex < len(bb.instructions); instIndex++ { if instIndex == 0 { + if r.maxDepth > 0 && depth >= r.maxDepth { + fmt.Printf("interp: depth exceeded in %v\n", fn.name) + return nil, mem, r.errorAt(fn.blocks[0].instructions[0], errDepthExceeded) + } + // This is the start of a new basic block. if len(mem.instructions) != startRTInsts { if _, ok := runtimeBlocks[lastBB]; ok { @@ -523,7 +528,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent } fmt.Fprintln(os.Stderr, indent+"call:", callFn.name+"("+strings.Join(argStrings, ", ")+")") } - retval, callMem, callErr := r.run(callFn, operands[1:], &mem, indent+" ") + retval, callMem, callErr := r.run(callFn, operands[1:], &mem, depth+1, indent+" ") if callErr != nil { if isRecoverableError(callErr.Err) { // This error can be recovered by doing the call at diff --git a/main.go b/main.go index 0a8a652b7..6d7da79be 100644 --- a/main.go +++ b/main.go @@ -1418,6 +1418,7 @@ func main() { serial := flag.String("serial", "", "which serial output to use (none, uart, usb)") work := flag.Bool("work", false, "print the name of the temporary build directory and do not delete this directory on exit") interpTimeout := flag.Duration("interp-timeout", 180*time.Second, "interp optimization pass timeout") + interpMaxDepth := flag.Int("interp-maxdepth", 0, "interp optimization max depth (default 0=disabled)") var tags buildutil.TagsFlag flag.Var(&tags, "tags", "a space-separated list of extra build tags") target := flag.String("target", "", "chip/board name or JSON target specification file") @@ -1528,6 +1529,7 @@ func main() { Serial: *serial, Work: *work, InterpTimeout: *interpTimeout, + InterpMaxDepth: *interpMaxDepth, PrintIR: *printIR, DumpSSA: *dumpSSA, VerifyIR: *verifyIR, diff --git a/main_test.go b/main_test.go index ae7aed564..b80916a7b 100644 --- a/main_test.go +++ b/main_test.go @@ -290,15 +290,16 @@ func emuCheck(t *testing.T, options compileopts.Options) { func optionsFromTarget(target string, sema chan struct{}) compileopts.Options { return compileopts.Options{ // GOOS/GOARCH are only used if target == "" - GOOS: goenv.Get("GOOS"), - GOARCH: goenv.Get("GOARCH"), - GOARM: goenv.Get("GOARM"), - Target: target, - Semaphore: sema, - InterpTimeout: 180 * time.Second, - Debug: true, - VerifyIR: true, - Opt: "z", + GOOS: goenv.Get("GOOS"), + GOARCH: goenv.Get("GOARCH"), + GOARM: goenv.Get("GOARM"), + Target: target, + Semaphore: sema, + InterpTimeout: 180 * time.Second, + InterpMaxDepth: 10, + Debug: true, + VerifyIR: true, + Opt: "z", } } @@ -308,13 +309,14 @@ func optionsFromTarget(target string, sema chan struct{}) compileopts.Options { func optionsFromOSARCH(osarch string, sema chan struct{}) compileopts.Options { parts := strings.Split(osarch, "/") options := compileopts.Options{ - GOOS: parts[0], - GOARCH: parts[1], - Semaphore: sema, - InterpTimeout: 180 * time.Second, - Debug: true, - VerifyIR: true, - Opt: "z", + GOOS: parts[0], + GOARCH: parts[1], + Semaphore: sema, + InterpTimeout: 180 * time.Second, + InterpMaxDepth: 10, + Debug: true, + VerifyIR: true, + Opt: "z", } if options.GOARCH == "arm" { options.GOARM = parts[2] diff --git a/monitor_test.go b/monitor_test.go index 18c223742..5b428a674 100644 --- a/monitor_test.go +++ b/monitor_test.go @@ -22,11 +22,12 @@ func TestTraceback(t *testing.T) { // Build a small binary that only panics. tmpdir := t.TempDir() config, err := builder.NewConfig(&compileopts.Options{ - GOOS: runtime.GOOS, - GOARCH: runtime.GOARCH, - Opt: "z", - InterpTimeout: time.Minute, - Debug: true, + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + Opt: "z", + InterpTimeout: time.Minute, + InterpMaxDepth: 10, + Debug: true, }) if err != nil { t.Fatal(err)