From 923a6f5873c3ae48289f9f3d092e13916b296691 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Thu, 31 Oct 2019 13:27:21 +0100 Subject: [PATCH] interp: add testing for scanning for side effects --- interp/scan.go | 15 +++++++ interp/scan_test.go | 87 +++++++++++++++++++++++++++++++++++++++++ interp/testdata/scan.ll | 53 +++++++++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 interp/scan_test.go create mode 100644 interp/testdata/scan.ll diff --git a/interp/scan.go b/interp/scan.go index 9b77c091..a96bcb58 100644 --- a/interp/scan.go +++ b/interp/scan.go @@ -6,6 +6,21 @@ import ( type sideEffectSeverity int +func (severity sideEffectSeverity) String() string { + switch severity { + case sideEffectInProgress: + return "in progress" + case sideEffectNone: + return "none" + case sideEffectLimited: + return "limited" + case sideEffectAll: + return "all" + default: + return "unknown" + } +} + const ( sideEffectInProgress sideEffectSeverity = iota // computing side effects is in progress (for recursive functions) sideEffectNone // no side effects at all (pure) diff --git a/interp/scan_test.go b/interp/scan_test.go new file mode 100644 index 00000000..226373ce --- /dev/null +++ b/interp/scan_test.go @@ -0,0 +1,87 @@ +package interp + +import ( + "os" + "sort" + "testing" + + "tinygo.org/x/go-llvm" +) + +var scanTestTable = []struct { + name string + severity sideEffectSeverity + mentionsGlobals []string +}{ + {"returnsConst", sideEffectNone, nil}, + {"returnsArg", sideEffectNone, nil}, + {"externalCallOnly", sideEffectNone, nil}, + {"externalCallAndReturn", sideEffectLimited, nil}, + {"externalCallBranch", sideEffectLimited, nil}, + {"readCleanGlobal", sideEffectNone, []string{"cleanGlobalInt"}}, + {"readDirtyGlobal", sideEffectLimited, []string{"dirtyGlobalInt"}}, + {"callFunctionPointer", sideEffectAll, []string{"functionPointer"}}, +} + +func TestScan(t *testing.T) { + t.Parallel() + + // Read the input IR. + path := "testdata/scan.ll" + ctx := llvm.NewContext() + buf, err := llvm.NewMemoryBufferFromFile(path) + os.Stat(path) // make sure this file is tracked by `go test` caching + if err != nil { + t.Fatalf("could not read file %s: %v", path, err) + } + mod, err := ctx.ParseIR(buf) + if err != nil { + t.Fatalf("could not load module:\n%v", err) + } + + // Check all to-be-tested functions. + for _, tc := range scanTestTable { + // Create an eval object, for testing. + e := &Eval{ + Mod: mod, + TargetData: llvm.NewTargetData(mod.DataLayout()), + dirtyGlobals: map[llvm.Value]struct{}{}, + } + + // Mark some globals dirty, for testing. + e.markDirty(mod.NamedGlobal("dirtyGlobalInt")) + + // Scan for side effects. + fn := mod.NamedFunction(tc.name) + if fn.IsNil() { + t.Errorf("scan test: could not find tested function %s in the IR", tc.name) + continue + } + result := e.hasSideEffects(fn) + + // Check whether the result is what we expect. + if result.severity != tc.severity { + t.Errorf("scan test: function %s should have severity %s but it has %s", tc.name, tc.severity, result.severity) + } + + // Check whether the mentioned globals match with what we'd expect. + mentionsGlobalNames := make([]string, 0, len(result.mentionsGlobals)) + for global := range result.mentionsGlobals { + mentionsGlobalNames = append(mentionsGlobalNames, global.Name()) + } + sort.Strings(mentionsGlobalNames) + globalsMismatch := false + if len(result.mentionsGlobals) != len(tc.mentionsGlobals) { + globalsMismatch = true + } else { + for i, globalName := range mentionsGlobalNames { + if tc.mentionsGlobals[i] != globalName { + globalsMismatch = true + } + } + } + if globalsMismatch { + t.Errorf("scan test: expected %s to mention globals %v, but it mentions globals %v", tc.name, tc.mentionsGlobals, mentionsGlobalNames) + } + } +} diff --git a/interp/testdata/scan.ll b/interp/testdata/scan.ll new file mode 100644 index 00000000..33ae3001 --- /dev/null +++ b/interp/testdata/scan.ll @@ -0,0 +1,53 @@ +target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" +target triple = "x86_64--linux" + +define i64 @returnsConst() { + ret i64 0 +} + +define i64 @returnsArg(i64 %arg) { + ret i64 %arg +} + +declare i64 @externalCall() + +define i64 @externalCallOnly() { + %result = call i64 @externalCall() + ret i64 0 +} + +define i64 @externalCallAndReturn() { + %result = call i64 @externalCall() + ret i64 %result +} + +define i64 @externalCallBranch() { + %result = call i64 @externalCall() + %zero = icmp eq i64 %result, 0 + br i1 %zero, label %if.then, label %if.done + +if.then: + ret i64 2 + +if.done: + ret i64 4 +} + +@cleanGlobalInt = global i64 5 +define i64 @readCleanGlobal() { + %global = load i64, i64* @cleanGlobalInt + ret i64 %global +} + +@dirtyGlobalInt = global i64 5 +define i64 @readDirtyGlobal() { + %global = load i64, i64* @dirtyGlobalInt + ret i64 %global +} + +@functionPointer = global i64()* null +define i64 @callFunctionPointer() { + %fp = load i64()*, i64()** @functionPointer + %result = call i64 %fp() + ret i64 %result +}