From 87c2ccb0b9c96e72ea2dba54b05441eede2e1b96 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Tue, 25 May 2021 22:33:59 +0200 Subject: [PATCH] compiler: add tests for starting a goroutine This commit adds a test for both WebAssembly and Cortex-M targets (which use a different way of goroutine lowering) to show how they lower goroutines. It makes it easier to show how the output changes in future commits. --- compiler/compiler_test.go | 94 +++++++----- compiler/testdata/goroutine-cortex-m-qemu.ll | 144 +++++++++++++++++++ compiler/testdata/goroutine-wasm.ll | 90 ++++++++++++ compiler/testdata/goroutine.go | 24 ++++ 4 files changed, 314 insertions(+), 38 deletions(-) create mode 100644 compiler/testdata/goroutine-cortex-m-qemu.ll create mode 100644 compiler/testdata/goroutine-wasm.ll create mode 100644 compiler/testdata/goroutine.go diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 394ef1c2..4149e149 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -19,6 +19,8 @@ var flagUpdate = flag.Bool("update", false, "update tests based on test output") // Basic tests for the compiler. Build some Go files and compare the output with // the expected LLVM IR for regression testing. func TestCompiler(t *testing.T) { + t.Parallel() + // Check LLVM version. llvmMajor, err := strconv.Atoi(strings.SplitN(llvm.Version, ".", 2)[0]) if err != nil { @@ -32,43 +34,55 @@ func TestCompiler(t *testing.T) { t.Skip("compiler tests require LLVM 11 or above, got LLVM ", llvm.Version) } - target, err := compileopts.LoadTarget("wasm") - if err != nil { - t.Fatal("failed to load target:", err) - } - config := &compileopts.Config{ - Options: &compileopts.Options{}, - Target: target, - } - compilerConfig := &Config{ - Triple: config.Triple(), - GOOS: config.GOOS(), - GOARCH: config.GOARCH(), - CodeModel: config.CodeModel(), - RelocationModel: config.RelocationModel(), - Scheduler: config.Scheduler(), - FuncImplementation: config.FuncImplementation(), - AutomaticStackSize: config.AutomaticStackSize(), - } - machine, err := NewTargetMachine(compilerConfig) - if err != nil { - t.Fatal("failed to create target machine:", err) + tests := []struct { + file string + target string + }{ + {"basic.go", ""}, + {"pointer.go", ""}, + {"slice.go", ""}, + {"string.go", ""}, + {"float.go", ""}, + {"interface.go", ""}, + {"func.go", ""}, + {"goroutine.go", "wasm"}, + {"goroutine.go", "cortex-m-qemu"}, } - tests := []string{ - "basic.go", - "pointer.go", - "slice.go", - "string.go", - "float.go", - "interface.go", - "func.go", - } + for _, tc := range tests { + name := tc.file + targetString := "wasm" + if tc.target != "" { + targetString = tc.target + name = tc.file + "-" + tc.target + } + + t.Run(name, func(t *testing.T) { + target, err := compileopts.LoadTarget(targetString) + if err != nil { + t.Fatal("failed to load target:", err) + } + config := &compileopts.Config{ + Options: &compileopts.Options{}, + Target: target, + } + compilerConfig := &Config{ + Triple: config.Triple(), + GOOS: config.GOOS(), + GOARCH: config.GOARCH(), + CodeModel: config.CodeModel(), + RelocationModel: config.RelocationModel(), + Scheduler: config.Scheduler(), + FuncImplementation: config.FuncImplementation(), + AutomaticStackSize: config.AutomaticStackSize(), + } + machine, err := NewTargetMachine(compilerConfig) + if err != nil { + t.Fatal("failed to create target machine:", err) + } - for _, testCase := range tests { - t.Run(testCase, func(t *testing.T) { // Load entire program AST into memory. - lprogram, err := loader.Load(config, []string{"./testdata/" + testCase}, config.ClangHeaders, types.Config{ + lprogram, err := loader.Load(config, []string{"./testdata/" + tc.file}, config.ClangHeaders, types.Config{ Sizes: Sizes(machine), }) if err != nil { @@ -76,13 +90,13 @@ func TestCompiler(t *testing.T) { } err = lprogram.Parse() if err != nil { - t.Fatalf("could not parse test case %s: %s", testCase, err) + t.Fatalf("could not parse test case %s: %s", tc.file, err) } // Compile AST to IR. program := lprogram.LoadSSA() pkg := lprogram.MainPkg() - mod, errs := CompilePackage(testCase, pkg, program.Package(pkg.Pkg), machine, compilerConfig, false) + mod, errs := CompilePackage(tc.file, pkg, program.Package(pkg.Pkg), machine, compilerConfig, false) if errs != nil { for _, err := range errs { t.Error(err) @@ -105,18 +119,22 @@ func TestCompiler(t *testing.T) { } funcPasses.FinalizeFunc() - outfile := "./testdata/" + testCase[:len(testCase)-3] + ".ll" + outFilePrefix := tc.file[:len(tc.file)-3] + if tc.target != "" { + outFilePrefix += "-" + tc.target + } + outPath := "./testdata/" + outFilePrefix + ".ll" // Update test if needed. Do not check the result. if *flagUpdate { - err := ioutil.WriteFile(outfile, []byte(mod.String()), 0666) + err := ioutil.WriteFile(outPath, []byte(mod.String()), 0666) if err != nil { t.Error("failed to write updated output file:", err) } return } - expected, err := ioutil.ReadFile(outfile) + expected, err := ioutil.ReadFile(outPath) if err != nil { t.Fatal("failed to read golden file:", err) } diff --git a/compiler/testdata/goroutine-cortex-m-qemu.ll b/compiler/testdata/goroutine-cortex-m-qemu.ll new file mode 100644 index 00000000..a729aff4 --- /dev/null +++ b/compiler/testdata/goroutine-cortex-m-qemu.ll @@ -0,0 +1,144 @@ +; ModuleID = 'goroutine.go' +source_filename = "goroutine.go" +target datalayout = "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64" +target triple = "armv7m-none-eabi" + +@"main.regularFunctionGoroutine$pack" = private unnamed_addr constant { i32, i8* } { i32 5, i8* undef } +@"main.inlineFunctionGoroutine$pack" = private unnamed_addr constant { i32, i8* } { i32 5, i8* undef } + +declare noalias nonnull i8* @runtime.alloc(i32, i8*, i8*) + +define hidden void @main.init(i8* %context, i8* %parentHandle) unnamed_addr { +entry: + ret void +} + +define hidden void @main.regularFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %stacksize = call i32 @"internal/task.getGoroutineStackSize"(i32 ptrtoint (void (i8*)* @"main.regularFunction$gowrapper" to i32), i8* undef, i8* undef) + call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.regularFunction$gowrapper" to i32), i8* bitcast ({ i32, i8* }* @"main.regularFunctionGoroutine$pack" to i8*), i32 %stacksize, i8* undef, i8* null) + ret void +} + +declare void @main.regularFunction(i32, i8*, i8*) + +define linkonce_odr void @"main.regularFunction$gowrapper"(i8* %0) unnamed_addr #0 { +entry: + %1 = bitcast i8* %0 to i32* + %2 = load i32, i32* %1, align 4 + %3 = getelementptr inbounds i8, i8* %0, i32 4 + %4 = bitcast i8* %3 to i8** + %5 = load i8*, i8** %4, align 4 + call void @main.regularFunction(i32 %2, i8* %5, i8* undef) + ret void +} + +declare i32 @"internal/task.getGoroutineStackSize"(i32, i8*, i8*) + +declare void @"internal/task.start"(i32, i8*, i32, i8*, i8*) + +define hidden void @main.inlineFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %stacksize = call i32 @"internal/task.getGoroutineStackSize"(i32 ptrtoint (void (i8*)* @"main.inlineFunctionGoroutine$1$gowrapper" to i32), i8* undef, i8* undef) + call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.inlineFunctionGoroutine$1$gowrapper" to i32), i8* bitcast ({ i32, i8* }* @"main.inlineFunctionGoroutine$pack" to i8*), i32 %stacksize, i8* undef, i8* null) + ret void +} + +define hidden void @"main.inlineFunctionGoroutine$1"(i32 %x, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + ret void +} + +define linkonce_odr void @"main.inlineFunctionGoroutine$1$gowrapper"(i8* %0) unnamed_addr #1 { +entry: + %1 = bitcast i8* %0 to i32* + %2 = load i32, i32* %1, align 4 + %3 = getelementptr inbounds i8, i8* %0, i32 4 + %4 = bitcast i8* %3 to i8** + %5 = load i8*, i8** %4, align 4 + call void @"main.inlineFunctionGoroutine$1"(i32 %2, i8* %5, i8* undef) + ret void +} + +define hidden void @main.closureFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %n = call i8* @runtime.alloc(i32 4, i8* undef, i8* null) + %0 = bitcast i8* %n to i32* + store i32 3, i32* %0, align 4 + %1 = call i8* @runtime.alloc(i32 8, i8* undef, i8* null) + %2 = bitcast i8* %1 to i32* + store i32 5, i32* %2, align 4 + %3 = getelementptr inbounds i8, i8* %1, i32 4 + %4 = bitcast i8* %3 to i8** + store i8* %n, i8** %4, align 4 + %stacksize = call i32 @"internal/task.getGoroutineStackSize"(i32 ptrtoint (void (i8*)* @"main.closureFunctionGoroutine$1$gowrapper" to i32), i8* undef, i8* undef) + call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @"main.closureFunctionGoroutine$1$gowrapper" to i32), i8* nonnull %1, i32 %stacksize, i8* undef, i8* null) + %5 = load i32, i32* %0, align 4 + call void @runtime.printint32(i32 %5, i8* undef, i8* null) + ret void +} + +define hidden void @"main.closureFunctionGoroutine$1"(i32 %x, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %0 = icmp eq i8* %context, null + br i1 %0, label %store.throw, label %store.next + +store.throw: ; preds = %entry + call void @runtime.nilPanic(i8* undef, i8* null) + unreachable + +store.next: ; preds = %entry + %unpack.ptr = bitcast i8* %context to i32* + store i32 7, i32* %unpack.ptr, align 4 + ret void +} + +define linkonce_odr void @"main.closureFunctionGoroutine$1$gowrapper"(i8* %0) unnamed_addr #2 { +entry: + %1 = bitcast i8* %0 to i32* + %2 = load i32, i32* %1, align 4 + %3 = getelementptr inbounds i8, i8* %0, i32 4 + %4 = bitcast i8* %3 to i8** + %5 = load i8*, i8** %4, align 4 + call void @"main.closureFunctionGoroutine$1"(i32 %2, i8* %5, i8* undef) + ret void +} + +declare void @runtime.printint32(i32, i8*, i8*) + +declare void @runtime.nilPanic(i8*, i8*) + +define hidden void @main.funcGoroutine(i8* %fn.context, void (i32, i8*, i8*)* %fn.funcptr, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %0 = call i8* @runtime.alloc(i32 12, i8* undef, i8* null) + %1 = bitcast i8* %0 to i32* + store i32 5, i32* %1, align 4 + %2 = getelementptr inbounds i8, i8* %0, i32 4 + %3 = bitcast i8* %2 to i8** + store i8* %fn.context, i8** %3, align 4 + %4 = getelementptr inbounds i8, i8* %0, i32 8 + %5 = bitcast i8* %4 to void (i32, i8*, i8*)** + store void (i32, i8*, i8*)* %fn.funcptr, void (i32, i8*, i8*)** %5, align 4 + %stacksize = call i32 @"internal/task.getGoroutineStackSize"(i32 ptrtoint (void (i8*)* @main.funcGoroutine.gowrapper to i32), i8* undef, i8* undef) + call void @"internal/task.start"(i32 ptrtoint (void (i8*)* @main.funcGoroutine.gowrapper to i32), i8* nonnull %0, i32 %stacksize, i8* undef, i8* null) + ret void +} + +define linkonce_odr void @main.funcGoroutine.gowrapper(i8* %0) unnamed_addr #3 { +entry: + %1 = bitcast i8* %0 to i32* + %2 = load i32, i32* %1, align 4 + %3 = getelementptr inbounds i8, i8* %0, i32 4 + %4 = bitcast i8* %3 to i8** + %5 = load i8*, i8** %4, align 4 + %6 = getelementptr inbounds i8, i8* %0, i32 8 + %7 = bitcast i8* %6 to void (i32, i8*, i8*)** + %8 = load void (i32, i8*, i8*)*, void (i32, i8*, i8*)** %7, align 4 + call void %8(i32 %2, i8* %5, i8* undef) + ret void +} + +attributes #0 = { "tinygo-gowrapper"="main.regularFunction" } +attributes #1 = { "tinygo-gowrapper"="main.inlineFunctionGoroutine$1" } +attributes #2 = { "tinygo-gowrapper"="main.closureFunctionGoroutine$1" } +attributes #3 = { "tinygo-gowrapper" } diff --git a/compiler/testdata/goroutine-wasm.ll b/compiler/testdata/goroutine-wasm.ll new file mode 100644 index 00000000..83dcae32 --- /dev/null +++ b/compiler/testdata/goroutine-wasm.ll @@ -0,0 +1,90 @@ +; ModuleID = 'goroutine.go' +source_filename = "goroutine.go" +target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128" +target triple = "wasm32--wasi" + +%runtime.funcValueWithSignature = type { i32, i8* } + +@"main.regularFunctionGoroutine$pack" = private unnamed_addr constant { i32, i8* } { i32 5, i8* undef } +@"main.inlineFunctionGoroutine$pack" = private unnamed_addr constant { i32, i8* } { i32 5, i8* undef } +@"reflect/types.funcid:func:{basic:int}{}" = external constant i8 +@"main.closureFunctionGoroutine$1$withSignature" = linkonce_odr constant %runtime.funcValueWithSignature { i32 ptrtoint (void (i32, i8*, i8*)* @"main.closureFunctionGoroutine$1" to i32), i8* @"reflect/types.funcid:func:{basic:int}{}" } + +declare noalias nonnull i8* @runtime.alloc(i32, i8*, i8*) + +define hidden void @main.init(i8* %context, i8* %parentHandle) unnamed_addr { +entry: + ret void +} + +define hidden void @main.regularFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr { +entry: + call void @"internal/task.start"(i32 ptrtoint (void (i32, i8*, i8*)* @main.regularFunction to i32), i8* bitcast ({ i32, i8* }* @"main.regularFunctionGoroutine$pack" to i8*), i32 undef, i8* undef, i8* null) + ret void +} + +declare void @main.regularFunction(i32, i8*, i8*) + +declare void @"internal/task.start"(i32, i8*, i32, i8*, i8*) + +define hidden void @main.inlineFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr { +entry: + call void @"internal/task.start"(i32 ptrtoint (void (i32, i8*, i8*)* @"main.inlineFunctionGoroutine$1" to i32), i8* bitcast ({ i32, i8* }* @"main.inlineFunctionGoroutine$pack" to i8*), i32 undef, i8* undef, i8* null) + ret void +} + +define hidden void @"main.inlineFunctionGoroutine$1"(i32 %x, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + ret void +} + +define hidden void @main.closureFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %n = call i8* @runtime.alloc(i32 4, i8* undef, i8* null) + %0 = bitcast i8* %n to i32* + store i32 3, i32* %0, align 4 + %1 = call i8* @runtime.alloc(i32 8, i8* undef, i8* null) + %2 = bitcast i8* %1 to i32* + store i32 5, i32* %2, align 4 + %3 = getelementptr inbounds i8, i8* %1, i32 4 + %4 = bitcast i8* %3 to i8** + store i8* %n, i8** %4, align 4 + call void @"internal/task.start"(i32 ptrtoint (void (i32, i8*, i8*)* @"main.closureFunctionGoroutine$1" to i32), i8* nonnull %1, i32 undef, i8* undef, i8* null) + %5 = load i32, i32* %0, align 4 + call void @runtime.printint32(i32 %5, i8* undef, i8* null) + ret void +} + +define hidden void @"main.closureFunctionGoroutine$1"(i32 %x, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %0 = icmp eq i8* %context, null + br i1 %0, label %store.throw, label %store.next + +store.throw: ; preds = %entry + call void @runtime.nilPanic(i8* undef, i8* null) + unreachable + +store.next: ; preds = %entry + %unpack.ptr = bitcast i8* %context to i32* + store i32 7, i32* %unpack.ptr, align 4 + ret void +} + +declare void @runtime.printint32(i32, i8*, i8*) + +declare void @runtime.nilPanic(i8*, i8*) + +define hidden void @main.funcGoroutine(i8* %fn.context, i32 %fn.funcptr, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %0 = call i32 @runtime.getFuncPtr(i8* %fn.context, i32 %fn.funcptr, i8* nonnull @"reflect/types.funcid:func:{basic:int}{}", i8* undef, i8* null) + %1 = call i8* @runtime.alloc(i32 8, i8* undef, i8* null) + %2 = bitcast i8* %1 to i32* + store i32 5, i32* %2, align 4 + %3 = getelementptr inbounds i8, i8* %1, i32 4 + %4 = bitcast i8* %3 to i8** + store i8* %fn.context, i8** %4, align 4 + call void @"internal/task.start"(i32 %0, i8* nonnull %1, i32 undef, i8* undef, i8* null) + ret void +} + +declare i32 @runtime.getFuncPtr(i8*, i32, i8* dereferenceable_or_null(1), i8*, i8*) diff --git a/compiler/testdata/goroutine.go b/compiler/testdata/goroutine.go new file mode 100644 index 00000000..2122d47f --- /dev/null +++ b/compiler/testdata/goroutine.go @@ -0,0 +1,24 @@ +package main + +func regularFunctionGoroutine() { + go regularFunction(5) +} + +func inlineFunctionGoroutine() { + go func(x int) { + }(5) +} + +func closureFunctionGoroutine() { + n := 3 + go func(x int) { + n = 7 + }(5) + print(n) // note: this is racy (but good enough for this test) +} + +func funcGoroutine(fn func(x int)) { + go fn(5) +} + +func regularFunction(x int)