diff --git a/compiler/goroutine.go b/compiler/goroutine.go index 630f93a9..d7ad6018 100644 --- a/compiler/goroutine.go +++ b/compiler/goroutine.go @@ -40,6 +40,37 @@ func (b *builder) createGo(instr *ssa.Go) { } params = append(params, context) // context parameter funcPtr = b.getFunction(callee) + } else if builtin, ok := instr.Call.Value.(*ssa.Builtin); ok { + // We cheat. None of the builtins do any long or blocking operation, so + // we might as well run these builtins right away without the program + // noticing the difference. + // Possible exceptions: + // - copy: this is a possibly long operation, but not a blocking + // operation. Semantically it makes no difference to run it right + // away (not in a goroutine). However, in practice it makes no sense + // to run copy in a goroutine as there is no way to (safely) know + // when it is finished. + // - panic: the error message would appear in the parent goroutine. + // But because `go panic("err")` would halt the program anyway + // (there is no recover), panicking right away would give the same + // behavior as creating a goroutine, switching the scheduler to that + // goroutine, and panicking there. So this optimization seems + // correct. + // - recover: because it runs in a new goroutine, it is never a + // deferred function. Thus this is a no-op. + if builtin.Name() == "recover" { + // This is a no-op, even in a deferred function: + // go recover() + return + } + var argTypes []types.Type + var argValues []llvm.Value + for _, arg := range instr.Call.Args { + argTypes = append(argTypes, arg.Type()) + argValues = append(argValues, b.getValue(arg)) + } + b.createBuiltin(argTypes, argValues, builtin.Name(), instr.Pos()) + return } else if !instr.Call.IsInvoke() { // This is a function pointer. // At the moment, two extra params are passed to the newly started diff --git a/compiler/testdata/goroutine-cortex-m-qemu.ll b/compiler/testdata/goroutine-cortex-m-qemu.ll index a729aff4..7ddfe569 100644 --- a/compiler/testdata/goroutine-cortex-m-qemu.ll +++ b/compiler/testdata/goroutine-cortex-m-qemu.ll @@ -3,6 +3,12 @@ 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" +%runtime.channel = type { i32, i32, i8, %runtime.channelBlockedList*, i32, i32, i32, i8* } +%runtime.channelBlockedList = type { %runtime.channelBlockedList*, %"internal/task.Task"*, %runtime.chanSelectState*, { %runtime.channelBlockedList*, i32, i32 } } +%"internal/task.Task" = type { %"internal/task.Task"*, i8*, i32, %"internal/task.state" } +%"internal/task.state" = type { i32, i32* } +%runtime.chanSelectState = type { %runtime.channel*, 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 } @@ -138,6 +144,27 @@ entry: ret void } +define hidden void @main.recoverBuiltinGoroutine(i8* %context, i8* %parentHandle) unnamed_addr { +entry: + ret void +} + +define hidden void @main.copyBuiltinGoroutine(i8* %dst.data, i32 %dst.len, i32 %dst.cap, i8* %src.data, i32 %src.len, i32 %src.cap, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %copy.n = call i32 @runtime.sliceCopy(i8* %dst.data, i8* %src.data, i32 %dst.len, i32 %src.len, i32 1, i8* undef, i8* null) + ret void +} + +declare i32 @runtime.sliceCopy(i8* nocapture writeonly, i8* nocapture readonly, i32, i32, i32, i8*, i8*) + +define hidden void @main.closeBuiltinGoroutine(%runtime.channel* dereferenceable_or_null(32) %ch, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + call void @runtime.chanClose(%runtime.channel* %ch, i8* undef, i8* null) + ret void +} + +declare void @runtime.chanClose(%runtime.channel* dereferenceable_or_null(32), i8*, i8*) + attributes #0 = { "tinygo-gowrapper"="main.regularFunction" } attributes #1 = { "tinygo-gowrapper"="main.inlineFunctionGoroutine$1" } attributes #2 = { "tinygo-gowrapper"="main.closureFunctionGoroutine$1" } diff --git a/compiler/testdata/goroutine-wasm.ll b/compiler/testdata/goroutine-wasm.ll index 83dcae32..c9607185 100644 --- a/compiler/testdata/goroutine-wasm.ll +++ b/compiler/testdata/goroutine-wasm.ll @@ -4,6 +4,11 @@ target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128" target triple = "wasm32--wasi" %runtime.funcValueWithSignature = type { i32, i8* } +%runtime.channel = type { i32, i32, i8, %runtime.channelBlockedList*, i32, i32, i32, i8* } +%runtime.channelBlockedList = type { %runtime.channelBlockedList*, %"internal/task.Task"*, %runtime.chanSelectState*, { %runtime.channelBlockedList*, i32, i32 } } +%"internal/task.Task" = type { %"internal/task.Task"*, i8*, i32, %"internal/task.state" } +%"internal/task.state" = type { i8* } +%runtime.chanSelectState = type { %runtime.channel*, 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 } @@ -88,3 +93,24 @@ entry: } declare i32 @runtime.getFuncPtr(i8*, i32, i8* dereferenceable_or_null(1), i8*, i8*) + +define hidden void @main.recoverBuiltinGoroutine(i8* %context, i8* %parentHandle) unnamed_addr { +entry: + ret void +} + +define hidden void @main.copyBuiltinGoroutine(i8* %dst.data, i32 %dst.len, i32 %dst.cap, i8* %src.data, i32 %src.len, i32 %src.cap, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + %copy.n = call i32 @runtime.sliceCopy(i8* %dst.data, i8* %src.data, i32 %dst.len, i32 %src.len, i32 1, i8* undef, i8* null) + ret void +} + +declare i32 @runtime.sliceCopy(i8* nocapture writeonly, i8* nocapture readonly, i32, i32, i32, i8*, i8*) + +define hidden void @main.closeBuiltinGoroutine(%runtime.channel* dereferenceable_or_null(32) %ch, i8* %context, i8* %parentHandle) unnamed_addr { +entry: + call void @runtime.chanClose(%runtime.channel* %ch, i8* undef, i8* null) + ret void +} + +declare void @runtime.chanClose(%runtime.channel* dereferenceable_or_null(32), i8*, i8*) diff --git a/compiler/testdata/goroutine.go b/compiler/testdata/goroutine.go index 2122d47f..5d33082a 100644 --- a/compiler/testdata/goroutine.go +++ b/compiler/testdata/goroutine.go @@ -21,4 +21,23 @@ func funcGoroutine(fn func(x int)) { go fn(5) } +func recoverBuiltinGoroutine() { + // This is a no-op. + go recover() +} + +func copyBuiltinGoroutine(dst, src []byte) { + // This is not run in a goroutine. While this copy operation can indeed take + // some time (if there is a lot of data to copy), there is no race-free way + // to make use of the result so it's unlikely applications will make use of + // it. And doing it this way should be just within the Go specification. + go copy(dst, src) +} + +func closeBuiltinGoroutine(ch chan int) { + // This builtin is executed directly, not in a goroutine. + // The observed behavior is the same. + go close(ch) +} + func regularFunction(x int) diff --git a/testdata/goroutines.go b/testdata/goroutines.go index 17b2b0e3..c23dbdb9 100644 --- a/testdata/goroutines.go +++ b/testdata/goroutines.go @@ -73,6 +73,8 @@ func main() { time.Sleep(2 * time.Millisecond) + testGoOnBuiltins() + testCond() } @@ -131,6 +133,31 @@ type simpleFunc func() func emptyFunc() { } +func testGoOnBuiltins() { + // Test copy builtin (there is no non-racy practical use of this). + go copy(make([]int, 8), []int{2, 5, 8, 4}) + + // Test recover builtin (no-op). + go recover() + + // Test close builtin. + ch := make(chan int) + go close(ch) + n, ok := <-ch + if n != 0 || ok != false { + println("error: expected closed channel to return 0, false") + } + + // Test delete builtin. + m := map[string]int{"foo": 3} + go delete(m, "foo") + time.Sleep(time.Millisecond) + v, ok := m["foo"] + if v != 0 || ok != false { + println("error: expected deleted map entry to be 0, false") + } +} + func testCond() { var cond runtime.Cond go func() {