Compare commits

...

2 Commits

Author SHA1 Message Date
Kenneth Bell ced1fa616c interp: limit max instructions per function 2023-09-22 09:56:43 +01:00
Kenneth Bell a73eac22f1 interp: add 'max depth' parameter to limit cost 2023-09-22 09:56:43 +01:00
9 changed files with 60 additions and 34 deletions
+2 -2
View File
@@ -446,7 +446,7 @@ func Build(pkgName, outpath, tmpdir string, config *compileopts.Config) (BuildRe
if pkgInit.IsNil() { if pkgInit.IsNil() {
panic("init not found for " + pkg.Pkg.Path()) 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.Options.InterpMaxInstr, config.DumpSSA())
if err != nil { if err != nil {
return err 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 // 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. // optional and must be run as the compiler expects them to run.
func optimizeProgram(mod llvm.Module, config *compileopts.Config) error { 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.Options.InterpMaxInstr, config.DumpSSA())
if err != nil { if err != nil {
return err return err
} }
+2
View File
@@ -32,6 +32,8 @@ type Options struct {
Serial string Serial string
Work bool // -work flag to print temporary build directory Work bool // -work flag to print temporary build directory
InterpTimeout time.Duration InterpTimeout time.Duration
InterpMaxDepth int
InterpMaxInstr int
PrintIR bool PrintIR bool
DumpSSA bool DumpSSA bool
VerifyIR bool VerifyIR bool
+2 -1
View File
@@ -19,6 +19,7 @@ var (
errUnsupportedRuntimeInst = errors.New("interp: unsupported instruction (to be emitted at runtime)") errUnsupportedRuntimeInst = errors.New("interp: unsupported instruction (to be emitted at runtime)")
errMapAlreadyCreated = errors.New("interp: map already created") errMapAlreadyCreated = errors.New("interp: map already created")
errLoopUnrolled = errors.New("interp: loop unrolled") 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 // 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 { func isRecoverableError(err error) bool {
return err == errIntegerAsPointer || err == errUnsupportedInst || return err == errIntegerAsPointer || err == errUnsupportedInst ||
err == errUnsupportedRuntimeInst || err == errMapAlreadyCreated || err == errUnsupportedRuntimeInst || err == errMapAlreadyCreated ||
err == errLoopUnrolled err == errLoopUnrolled || err == errDepthExceeded
} }
// ErrorLine is one line in a traceback. The position may be missing. // ErrorLine is one line in a traceback. The position may be missing.
+11 -7
View File
@@ -31,10 +31,12 @@ type runner struct {
globals map[llvm.Value]int // map from global to index in objects slice globals map[llvm.Value]int // map from global to index in objects slice
start time.Time start time.Time
timeout time.Duration timeout time.Duration
maxDepth int
maxInstr int
callsExecuted uint64 callsExecuted uint64
} }
func newRunner(mod llvm.Module, timeout time.Duration, debug bool) *runner { func newRunner(mod llvm.Module, timeout time.Duration, maxDepth int, maxInstr int, debug bool) *runner {
r := runner{ r := runner{
mod: mod, mod: mod,
targetData: llvm.NewTargetData(mod.DataLayout()), targetData: llvm.NewTargetData(mod.DataLayout()),
@@ -44,6 +46,8 @@ func newRunner(mod llvm.Module, timeout time.Duration, debug bool) *runner {
globals: make(map[llvm.Value]int), globals: make(map[llvm.Value]int),
start: time.Now(), start: time.Now(),
timeout: timeout, timeout: timeout,
maxDepth: maxDepth,
maxInstr: maxInstr,
} }
r.pointerSize = uint32(r.targetData.PointerSize()) r.pointerSize = uint32(r.targetData.PointerSize())
r.i8ptrType = llvm.PointerType(mod.Context().Int8Type(), 0) r.i8ptrType = llvm.PointerType(mod.Context().Int8Type(), 0)
@@ -60,8 +64,8 @@ func (r *runner) dispose() {
// Run evaluates runtime.initAll function as much as possible at compile time. // Run evaluates runtime.initAll function as much as possible at compile time.
// Set debug to true if it should print output while running. // Set debug to true if it should print output while running.
func Run(mod llvm.Module, timeout time.Duration, debug bool) error { func Run(mod llvm.Module, timeout time.Duration, maxDepth int, maxInstr int, debug bool) error {
r := newRunner(mod, timeout, debug) r := newRunner(mod, timeout, maxDepth, maxInstr, debug)
defer r.dispose() defer r.dispose()
initAll := mod.NamedFunction("runtime.initAll") initAll := mod.NamedFunction("runtime.initAll")
@@ -114,7 +118,7 @@ func Run(mod llvm.Module, timeout time.Duration, debug bool) error {
if r.debug { if r.debug {
fmt.Fprintln(os.Stderr, "call:", fn.Name()) 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() call.EraseFromParentAsInstruction()
if callErr != nil { if callErr != nil {
if isRecoverableError(callErr.Err) { if isRecoverableError(callErr.Err) {
@@ -201,10 +205,10 @@ func Run(mod llvm.Module, timeout time.Duration, debug bool) error {
// RunFunc evaluates a single package initializer at compile time. // RunFunc evaluates a single package initializer at compile time.
// Set debug to true if it should print output while running. // 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, maxInstr int, debug bool) error {
// Create and initialize *runner object. // Create and initialize *runner object.
mod := fn.GlobalParent() mod := fn.GlobalParent()
r := newRunner(mod, timeout, debug) r := newRunner(mod, timeout, maxDepth, maxInstr, debug)
defer r.dispose() defer r.dispose()
initName := fn.Name() initName := fn.Name()
if !strings.HasSuffix(initName, ".init") { if !strings.HasSuffix(initName, ".init") {
@@ -235,7 +239,7 @@ func RunFunc(fn llvm.Value, timeout time.Duration, debug bool) error {
if r.debug { if r.debug {
fmt.Fprintln(os.Stderr, "interp:", fn.Name()) 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 callErr != nil {
if isRecoverableError(callErr.Err) { if isRecoverableError(callErr.Err) {
// Could not finish, but could recover from it. // Could not finish, but could recover from it.
+1 -1
View File
@@ -53,7 +53,7 @@ func runTest(t *testing.T, pathPrefix string) {
defer mod.Dispose() defer mod.Dispose()
// Perform the transform. // Perform the transform.
err = Run(mod, 10*time.Minute, false) err = Run(mod, 10*time.Minute, 10, 0, false)
if err != nil { if err != nil {
if err, match := err.(*Error); match { if err, match := err.(*Error); match {
println(err.Error()) println(err.Error())
+14 -2
View File
@@ -12,7 +12,7 @@ import (
"tinygo.org/x/go-llvm" "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} mem := memoryView{r: r, parent: parentMem}
locals := make([]value, len(fn.locals)) locals := make([]value, len(fn.locals))
r.callsExecuted++ r.callsExecuted++
@@ -33,8 +33,20 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent
lastBB := -1 // last basic block is undefined, only defined after a branch lastBB := -1 // last basic block is undefined, only defined after a branch
var operands []value var operands []value
startRTInsts := len(mem.instructions) startRTInsts := len(mem.instructions)
instCount := 0
for instIndex := 0; instIndex < len(bb.instructions); instIndex++ { for instIndex := 0; instIndex < len(bb.instructions); instIndex++ {
instCount++
if r.maxInstr > 0 && instCount > r.maxInstr {
fmt.Printf("interp: excess instructions evaluated in %v\n", fn.name)
return nil, mem, r.errorAt(fn.blocks[0].instructions[0], errDepthExceeded)
}
if instIndex == 0 { 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. // This is the start of a new basic block.
if len(mem.instructions) != startRTInsts { if len(mem.instructions) != startRTInsts {
if _, ok := runtimeBlocks[lastBB]; ok { if _, ok := runtimeBlocks[lastBB]; ok {
@@ -523,7 +535,7 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent
} }
fmt.Fprintln(os.Stderr, indent+"call:", callFn.name+"("+strings.Join(argStrings, ", ")+")") 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 callErr != nil {
if isRecoverableError(callErr.Err) { if isRecoverableError(callErr.Err) {
// This error can be recovered by doing the call at // This error can be recovered by doing the call at
+4
View File
@@ -1418,6 +1418,8 @@ func main() {
serial := flag.String("serial", "", "which serial output to use (none, uart, usb)") 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") 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") 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)")
interpMaxInstr := flag.Int("interp-maxinstr", 100_000, "limit interp optimization max instructions (0=disabled)")
var tags buildutil.TagsFlag var tags buildutil.TagsFlag
flag.Var(&tags, "tags", "a space-separated list of extra build tags") flag.Var(&tags, "tags", "a space-separated list of extra build tags")
target := flag.String("target", "", "chip/board name or JSON target specification file") target := flag.String("target", "", "chip/board name or JSON target specification file")
@@ -1528,6 +1530,8 @@ func main() {
Serial: *serial, Serial: *serial,
Work: *work, Work: *work,
InterpTimeout: *interpTimeout, InterpTimeout: *interpTimeout,
InterpMaxDepth: *interpMaxDepth,
InterpMaxInstr: *interpMaxInstr,
PrintIR: *printIR, PrintIR: *printIR,
DumpSSA: *dumpSSA, DumpSSA: *dumpSSA,
VerifyIR: *verifyIR, VerifyIR: *verifyIR,
+18 -16
View File
@@ -290,15 +290,16 @@ func emuCheck(t *testing.T, options compileopts.Options) {
func optionsFromTarget(target string, sema chan struct{}) compileopts.Options { func optionsFromTarget(target string, sema chan struct{}) compileopts.Options {
return compileopts.Options{ return compileopts.Options{
// GOOS/GOARCH are only used if target == "" // GOOS/GOARCH are only used if target == ""
GOOS: goenv.Get("GOOS"), GOOS: goenv.Get("GOOS"),
GOARCH: goenv.Get("GOARCH"), GOARCH: goenv.Get("GOARCH"),
GOARM: goenv.Get("GOARM"), GOARM: goenv.Get("GOARM"),
Target: target, Target: target,
Semaphore: sema, Semaphore: sema,
InterpTimeout: 180 * time.Second, InterpTimeout: 180 * time.Second,
Debug: true, InterpMaxDepth: 10,
VerifyIR: true, Debug: true,
Opt: "z", 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 { func optionsFromOSARCH(osarch string, sema chan struct{}) compileopts.Options {
parts := strings.Split(osarch, "/") parts := strings.Split(osarch, "/")
options := compileopts.Options{ options := compileopts.Options{
GOOS: parts[0], GOOS: parts[0],
GOARCH: parts[1], GOARCH: parts[1],
Semaphore: sema, Semaphore: sema,
InterpTimeout: 180 * time.Second, InterpTimeout: 180 * time.Second,
Debug: true, InterpMaxDepth: 10,
VerifyIR: true, Debug: true,
Opt: "z", VerifyIR: true,
Opt: "z",
} }
if options.GOARCH == "arm" { if options.GOARCH == "arm" {
options.GOARM = parts[2] options.GOARM = parts[2]
+6 -5
View File
@@ -22,11 +22,12 @@ func TestTraceback(t *testing.T) {
// Build a small binary that only panics. // Build a small binary that only panics.
tmpdir := t.TempDir() tmpdir := t.TempDir()
config, err := builder.NewConfig(&compileopts.Options{ config, err := builder.NewConfig(&compileopts.Options{
GOOS: runtime.GOOS, GOOS: runtime.GOOS,
GOARCH: runtime.GOARCH, GOARCH: runtime.GOARCH,
Opt: "z", Opt: "z",
InterpTimeout: time.Minute, InterpTimeout: time.Minute,
Debug: true, InterpMaxDepth: 10,
Debug: true,
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)