diff --git a/builder/build.go b/builder/build.go index 325f6b4906..4987fdddc6 100644 --- a/builder/build.go +++ b/builder/build.go @@ -489,7 +489,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.InterpMaxLoopIterations, config.DumpSSA()) if err != nil { return err } @@ -1208,7 +1208,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.InterpMaxLoopIterations, config.DumpSSA()) if err != nil { return err } diff --git a/compileopts/options.go b/compileopts/options.go index e543dca459..c8f9d292b8 100644 --- a/compileopts/options.go +++ b/compileopts/options.go @@ -21,45 +21,46 @@ var ( // usually passed from the command line, but can also be passed in environment // variables for example. type Options struct { - GOOS string // environment variable - GOARCH string // environment variable - GOARM string // environment variable (only used with GOARCH=arm) - GOMIPS string // environment variable (only used with GOARCH=mips and GOARCH=mipsle) - Directory string // working dir, leave it unset to use the current working dir - Target string - BuildMode string // -buildmode flag - Opt string - GC string - PanicStrategy string - Scheduler string - StackSize uint64 // goroutine stack size (if none could be automatically determined) - Serial string - Work bool // -work flag to print temporary build directory - InterpTimeout time.Duration - PrintIR bool - DumpSSA bool - VerifyIR bool - SkipDWARF bool - PrintCommands func(cmd string, args ...string) `json:"-"` - Semaphore chan struct{} `json:"-"` // -p flag controls cap - Debug bool - Nobounds bool - PrintSizes string - PrintAllocs *regexp.Regexp // regexp string - PrintStacks bool - Tags []string - GlobalValues map[string]map[string]string // map[pkgpath]map[varname]value - TestConfig TestConfig - Programmer string - OpenOCDCommands []string - LLVMFeatures string - Monitor bool - BaudRate int - Timeout time.Duration - WITPackage string // pass through to wasm-tools component embed invocation - WITWorld string // pass through to wasm-tools component embed -w option - ExtLDFlags []string - GoCompatibility bool // enable to check for Go version compatibility + GOOS string // environment variable + GOARCH string // environment variable + GOARM string // environment variable (only used with GOARCH=arm) + GOMIPS string // environment variable (only used with GOARCH=mips and GOARCH=mipsle) + Directory string // working dir, leave it unset to use the current working dir + Target string + BuildMode string // -buildmode flag + Opt string + GC string + PanicStrategy string + Scheduler string + StackSize uint64 // goroutine stack size (if none could be automatically determined) + Serial string + Work bool // -work flag to print temporary build directory + InterpTimeout time.Duration + InterpMaxLoopIterations int + PrintIR bool + DumpSSA bool + VerifyIR bool + SkipDWARF bool + PrintCommands func(cmd string, args ...string) `json:"-"` + Semaphore chan struct{} `json:"-"` // -p flag controls cap + Debug bool + Nobounds bool + PrintSizes string + PrintAllocs *regexp.Regexp // regexp string + PrintStacks bool + Tags []string + GlobalValues map[string]map[string]string // map[pkgpath]map[varname]value + TestConfig TestConfig + Programmer string + OpenOCDCommands []string + LLVMFeatures string + Monitor bool + BaudRate int + Timeout time.Duration + WITPackage string // pass through to wasm-tools component embed invocation + WITWorld string // pass through to wasm-tools component embed -w option + ExtLDFlags []string + GoCompatibility bool // enable to check for Go version compatibility } // Verify performs a validation on the given options, raising an error if options are not valid. diff --git a/interp/errors.go b/interp/errors.go index f6bfb5368d..37a8e6d7d7 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") + errLoopTooLong = errors.New("interp: loop ran too many iterations") ) // 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 == errInvalidPtrToIntSize + err == errLoopUnrolled || err == errLoopTooLong || err == errInvalidPtrToIntSize } // ErrorLine is one line in a traceback. The position may be missing. diff --git a/interp/interp.go b/interp/interp.go index 30b0872485..424c388a11 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -19,35 +19,37 @@ const checks = true // runner contains all state related to one interp run. type runner struct { - mod llvm.Module - targetData llvm.TargetData - builder llvm.Builder - pointerSize uint32 // cached pointer size from the TargetData - dataPtrType llvm.Type // often used type so created in advance - uintptrType llvm.Type // equivalent to uintptr in Go - maxAlign int // maximum alignment of an object, alignment of runtime.alloc() result - byteOrder binary.ByteOrder // big-endian or little-endian - debug bool // log debug messages - pkgName string // package name of the currently executing package - functionCache map[llvm.Value]*function // cache of compiled functions - objects []object // slice of objects in memory - globals map[llvm.Value]int // map from global to index in objects slice - start time.Time - timeout time.Duration - callsExecuted uint64 + mod llvm.Module + targetData llvm.TargetData + builder llvm.Builder + pointerSize uint32 // cached pointer size from the TargetData + dataPtrType llvm.Type // often used type so created in advance + uintptrType llvm.Type // equivalent to uintptr in Go + maxAlign int // maximum alignment of an object, alignment of runtime.alloc() result + byteOrder binary.ByteOrder // big-endian or little-endian + debug bool // log debug messages + pkgName string // package name of the currently executing package + functionCache map[llvm.Value]*function // cache of compiled functions + objects []object // slice of objects in memory + globals map[llvm.Value]int // map from global to index in objects slice + start time.Time + timeout time.Duration + maxLoopIterations int + callsExecuted uint64 } -func newRunner(mod llvm.Module, timeout time.Duration, debug bool) *runner { +func newRunner(mod llvm.Module, timeout time.Duration, maxLoopIterations int, debug bool) *runner { r := runner{ - mod: mod, - targetData: llvm.NewTargetData(mod.DataLayout()), - byteOrder: llvmutil.ByteOrder(mod.Target()), - debug: debug, - functionCache: make(map[llvm.Value]*function), - objects: []object{{}}, - globals: make(map[llvm.Value]int), - start: time.Now(), - timeout: timeout, + mod: mod, + targetData: llvm.NewTargetData(mod.DataLayout()), + byteOrder: llvmutil.ByteOrder(mod.Target()), + debug: debug, + functionCache: make(map[llvm.Value]*function), + objects: []object{{}}, + globals: make(map[llvm.Value]int), + start: time.Now(), + timeout: timeout, + maxLoopIterations: maxLoopIterations, } r.pointerSize = uint32(r.targetData.PointerSize()) r.dataPtrType = llvm.PointerType(mod.Context().Int8Type(), 0) @@ -64,8 +66,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, maxLoopIterations int, debug bool) error { + r := newRunner(mod, timeout, maxLoopIterations, debug) defer r.dispose() initAll := mod.NamedFunction("runtime.initAll") @@ -204,10 +206,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, maxLoopIterations int, debug bool) error { // Create and initialize *runner object. mod := fn.GlobalParent() - r := newRunner(mod, timeout, debug) + r := newRunner(mod, timeout, maxLoopIterations, debug) defer r.dispose() initName := fn.Name() if !strings.HasSuffix(initName, ".init") { diff --git a/interp/interp_test.go b/interp/interp_test.go index f5bdb81882..52ffb95a28 100644 --- a/interp/interp_test.go +++ b/interp/interp_test.go @@ -44,7 +44,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, DefaultMaxInterpBlockEntries, false) if err != nil { if err, match := err.(*Error); match { println(err.Error()) diff --git a/interp/interpreter.go b/interp/interpreter.go index e8f5545d5d..4b46df249e 100644 --- a/interp/interpreter.go +++ b/interp/interpreter.go @@ -12,6 +12,12 @@ import ( "tinygo.org/x/go-llvm" ) +// DefaultMaxInterpBlockEntries is the default maximum number of times a single +// basic block may be entered during interpretation of one function call. This +// limits how far the interpreter will unroll or evaluate loops before deferring +// the init function to runtime. +const DefaultMaxInterpBlockEntries = 1000 + func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent string) (value, memoryView, *Error) { mem := memoryView{r: r, parent: parentMem} locals := make([]value, len(fn.locals)) @@ -26,6 +32,10 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent // This is used to prevent unrolling. var runtimeBlocks map[int]struct{} + // Track how many times each basic block has been entered, to detect + // loops that are too expensive to evaluate at compile time. + var blockCounts map[int]int + // Start with the first basic block and the first instruction. // Branch instructions may modify both bb and instIndex when branching. bb := fn.blocks[0] @@ -36,6 +46,18 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent for instIndex := 0; instIndex < len(bb.instructions); instIndex++ { if instIndex == 0 { // This is the start of a new basic block. + + // Check whether this block has been entered too many times, + // which indicates an expensive loop that should be deferred + // to runtime. + if blockCounts == nil { + blockCounts = make(map[int]int) + } + blockCounts[currentBB]++ + if r.maxLoopIterations > 0 && blockCounts[currentBB] > r.maxLoopIterations { + return nil, mem, r.errorAt(bb.instructions[0], errLoopTooLong) + } + if len(mem.instructions) != startRTInsts { if _, ok := runtimeBlocks[lastBB]; ok { // This loop has been unrolled. diff --git a/main.go b/main.go index de7b060f23..b2b9bb7be2 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,7 @@ import ( "github.com/tinygo-org/tinygo/compileopts" "github.com/tinygo-org/tinygo/diagnostics" "github.com/tinygo-org/tinygo/goenv" + "github.com/tinygo-org/tinygo/interp" "github.com/tinygo-org/tinygo/loader" "golang.org/x/tools/go/buildutil" "tinygo.org/x/espflasher/pkg/espflasher" @@ -1738,6 +1739,7 @@ func main() { serial := flag.String("serial", "", "which serial output to use (none, uart, usb, rtt)") 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") + interpLoopLimit := flag.Int("interp-loop-limit", interp.DefaultMaxInterpBlockEntries, "maximum loop iterations during interp (0 to disable)") 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") @@ -1855,42 +1857,43 @@ func main() { } options := &compileopts.Options{ - GOOS: goenv.Get("GOOS"), - GOARCH: goenv.Get("GOARCH"), - GOARM: goenv.Get("GOARM"), - GOMIPS: goenv.Get("GOMIPS"), - Target: *target, - BuildMode: *buildMode, - StackSize: stackSize, - Opt: *opt, - GC: *gc, - PanicStrategy: *panicStrategy, - Scheduler: *scheduler, - Serial: *serial, - Work: *work, - InterpTimeout: *interpTimeout, - PrintIR: *printIR, - DumpSSA: *dumpSSA, - VerifyIR: *verifyIR, - SkipDWARF: *skipDwarf, - Semaphore: make(chan struct{}, *parallelism), - Debug: !*nodebug, - Nobounds: *nobounds, - PrintSizes: *printSize, - PrintStacks: *printStacks, - PrintAllocs: printAllocs, - Tags: []string(tags), - TestConfig: testConfig, - GlobalValues: globalVarValues, - Programmer: *programmer, - OpenOCDCommands: ocdCommands, - LLVMFeatures: *llvmFeatures, - Monitor: *monitor, - BaudRate: *baudrate, - Timeout: *timeout, - WITPackage: witPackage, - WITWorld: witWorld, - GoCompatibility: *gocompatibility, + GOOS: goenv.Get("GOOS"), + GOARCH: goenv.Get("GOARCH"), + GOARM: goenv.Get("GOARM"), + GOMIPS: goenv.Get("GOMIPS"), + Target: *target, + BuildMode: *buildMode, + StackSize: stackSize, + Opt: *opt, + GC: *gc, + PanicStrategy: *panicStrategy, + Scheduler: *scheduler, + Serial: *serial, + Work: *work, + InterpTimeout: *interpTimeout, + InterpMaxLoopIterations: *interpLoopLimit, + PrintIR: *printIR, + DumpSSA: *dumpSSA, + VerifyIR: *verifyIR, + SkipDWARF: *skipDwarf, + Semaphore: make(chan struct{}, *parallelism), + Debug: !*nodebug, + Nobounds: *nobounds, + PrintSizes: *printSize, + PrintStacks: *printStacks, + PrintAllocs: printAllocs, + Tags: []string(tags), + TestConfig: testConfig, + GlobalValues: globalVarValues, + Programmer: *programmer, + OpenOCDCommands: ocdCommands, + LLVMFeatures: *llvmFeatures, + Monitor: *monitor, + BaudRate: *baudrate, + Timeout: *timeout, + WITPackage: witPackage, + WITWorld: witWorld, + GoCompatibility: *gocompatibility, } if *printCommands { options.PrintCommands = printCommand