diff --git a/compileopts/config.go b/compileopts/config.go index d7e4abe2..8537a0a6 100644 --- a/compileopts/config.go +++ b/compileopts/config.go @@ -114,7 +114,7 @@ func (c *Config) NeedsStackObjects() bool { } // Scheduler returns the scheduler implementation. Valid values are "none", -//"coroutines" and "tasks". +// "asyncify" and "tasks". func (c *Config) Scheduler() string { if c.Options.Scheduler != "" { return c.Options.Scheduler @@ -122,8 +122,8 @@ func (c *Config) Scheduler() string { if c.Target.Scheduler != "" { return c.Target.Scheduler } - // Fall back to coroutines, which are supported everywhere. - return "coroutines" + // Fall back to none. + return "none" } // Serial returns the serial implementation for this build configuration: uart, @@ -171,13 +171,10 @@ func (c *Config) FuncImplementation() string { // being pointed to doesn't need a context. The function pointer is a // regular function pointer. return "doubleword" - case "none", "coroutines": + case "none": // As "doubleword", but with the function pointer replaced by a unique // ID per function signature. Function values are called by using a // switch statement and choosing which function to call. - // Pick the switch implementation with the coroutines scheduler, as it - // allows the use of blocking inside a function that is used as a func - // value. return "switch" default: panic("unknown scheduler type") diff --git a/compileopts/options.go b/compileopts/options.go index a82dfcf9..aa914ce9 100644 --- a/compileopts/options.go +++ b/compileopts/options.go @@ -8,7 +8,7 @@ import ( var ( validGCOptions = []string{"none", "leaking", "conservative"} - validSchedulerOptions = []string{"none", "tasks", "coroutines", "asyncify"} + validSchedulerOptions = []string{"none", "tasks", "asyncify"} validSerialOptions = []string{"none", "uart", "usb"} validPrintSizeOptions = []string{"none", "short", "full"} validPanicStrategyOptions = []string{"print", "trap"} diff --git a/compileopts/options_test.go b/compileopts/options_test.go index e260e8b1..2845be3a 100644 --- a/compileopts/options_test.go +++ b/compileopts/options_test.go @@ -10,7 +10,7 @@ import ( func TestVerifyOptions(t *testing.T) { expectedGCError := errors.New(`invalid gc option 'incorrect': valid values are none, leaking, conservative`) - expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, coroutines, asyncify`) + expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify`) expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full`) expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`) @@ -67,12 +67,6 @@ func TestVerifyOptions(t *testing.T) { Scheduler: "tasks", }, }, - { - name: "SchedulerOptionCoroutines", - opts: compileopts.Options{ - Scheduler: "coroutines", - }, - }, { name: "InvalidPrintSizeOption", opts: compileopts.Options{ diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index ef22faca..c929231c 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -55,10 +55,9 @@ func TestCompiler(t *testing.T) { {"string.go", "", ""}, {"float.go", "", ""}, {"interface.go", "", ""}, - {"func.go", "", "coroutines"}, + {"func.go", "", "none"}, {"pragma.go", "", ""}, {"goroutine.go", "wasm", "asyncify"}, - {"goroutine.go", "wasm", "coroutines"}, {"goroutine.go", "cortex-m-qemu", "tasks"}, {"channel.go", "", ""}, {"intrinsics.go", "cortex-m-qemu", ""}, diff --git a/compiler/goroutine.go b/compiler/goroutine.go index a77a9b86..06ada25f 100644 --- a/compiler/goroutine.go +++ b/compiler/goroutine.go @@ -30,11 +30,6 @@ func (b *builder) createGo(instr *ssa.Go) { switch value := instr.Call.Value.(type) { case *ssa.Function: // Goroutine call is regular function call. No context is necessary. - if b.Scheduler == "coroutines" { - // The context parameter is assumed to be always present in the - // coroutines scheduler. - context = llvm.Undef(b.i8ptrType) - } case *ssa.MakeClosure: // A goroutine call on a func value, but the callee is trivial to find. For // example: immediately applied functions. @@ -98,7 +93,7 @@ func (b *builder) createGo(instr *ssa.Go) { params = append(params, context) // context parameter hasContext = true switch b.Scheduler { - case "none", "coroutines": + case "none": // There are no additional parameters needed for the goroutine start operation. case "tasks", "asyncify": // Add the function pointer as a parameter to start the goroutine. @@ -110,32 +105,22 @@ func (b *builder) createGo(instr *ssa.Go) { } paramBundle := b.emitPointerPack(params) - var callee, stackSize llvm.Value - switch b.Scheduler { - case "none", "tasks", "asyncify": - callee = b.createGoroutineStartWrapper(funcPtr, prefix, hasContext, instr.Pos()) - if b.AutomaticStackSize { - // The stack size is not known until after linking. Call a dummy - // function that will be replaced with a load from a special ELF - // section that contains the stack size (and is modified after - // linking). - stackSizeFn := b.getFunction(b.program.ImportedPackage("internal/task").Members["getGoroutineStackSize"].(*ssa.Function)) - stackSize = b.createCall(stackSizeFn, []llvm.Value{callee, llvm.Undef(b.i8ptrType), llvm.Undef(b.i8ptrType)}, "stacksize") - } else { - // The stack size is fixed at compile time. By emitting it here as a - // constant, it can be optimized. - if (b.Scheduler == "tasks" || b.Scheduler == "asyncify") && b.DefaultStackSize == 0 { - b.addError(instr.Pos(), "default stack size for goroutines is not set") - } - stackSize = llvm.ConstInt(b.uintptrType, b.DefaultStackSize, false) + var stackSize llvm.Value + callee := b.createGoroutineStartWrapper(funcPtr, prefix, hasContext, instr.Pos()) + if b.AutomaticStackSize { + // The stack size is not known until after linking. Call a dummy + // function that will be replaced with a load from a special ELF + // section that contains the stack size (and is modified after + // linking). + stackSizeFn := b.getFunction(b.program.ImportedPackage("internal/task").Members["getGoroutineStackSize"].(*ssa.Function)) + stackSize = b.createCall(stackSizeFn, []llvm.Value{callee, llvm.Undef(b.i8ptrType), llvm.Undef(b.i8ptrType)}, "stacksize") + } else { + // The stack size is fixed at compile time. By emitting it here as a + // constant, it can be optimized. + if (b.Scheduler == "tasks" || b.Scheduler == "asyncify") && b.DefaultStackSize == 0 { + b.addError(instr.Pos(), "default stack size for goroutines is not set") } - case "coroutines": - callee = b.CreatePtrToInt(funcPtr, b.uintptrType, "") - // There is no goroutine stack size: coroutines are used instead of - // stacks. - stackSize = llvm.Undef(b.uintptrType) - default: - panic("unreachable") + stackSize = llvm.ConstInt(b.uintptrType, b.DefaultStackSize, false) } start := b.getFunction(b.program.ImportedPackage("internal/task").Members["start"].(*ssa.Function)) b.createCall(start, []llvm.Value{callee, paramBundle, stackSize, llvm.Undef(b.i8ptrType), llvm.ConstPointerNull(b.i8ptrType)}, "") @@ -297,8 +282,8 @@ func (c *compilerContext) createGoroutineStartWrapper(fn llvm.Value, prefix stri // The last parameter in the packed object has somewhat of a dual role. // Inside the parameter bundle it's the function pointer, stored right // after the context pointer. But in the IR call instruction, it's the - // parentHandle function that's always undef outside of the coroutines - // scheduler. Thus, make the parameter undef here. + // parentHandle function that's always undef. Thus, make the parameter + // undef here. params[len(params)-1] = llvm.Undef(c.i8ptrType) // Create the call. diff --git a/compiler/testdata/func-coroutines.ll b/compiler/testdata/func-none.ll similarity index 100% rename from compiler/testdata/func-coroutines.ll rename to compiler/testdata/func-none.ll diff --git a/compiler/testdata/goroutine-wasm-coroutines.ll b/compiler/testdata/goroutine-wasm-coroutines.ll deleted file mode 100644 index 42a7d6e1..00000000 --- a/compiler/testdata/goroutine-wasm-coroutines.ll +++ /dev/null @@ -1,149 +0,0 @@ -; ModuleID = 'goroutine.go' -source_filename = "goroutine.go" -target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128-ni:1:10:20" -target triple = "wasm32-unknown-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*, i64, %"internal/task.gcData", %"internal/task.state" } -%"internal/task.gcData" = type {} -%"internal/task.state" = type { i8* } -%runtime.chanSelectState = type { %runtime.channel*, i8* } - -@"main$pack" = internal unnamed_addr constant { i32, i8* } { i32 5, i8* undef } -@"main$pack.1" = internal 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}{}" } -@"main$string" = internal unnamed_addr constant [4 x i8] c"test", align 1 - -declare noalias nonnull i8* @runtime.alloc(i32, i8*, i8*, i8*) - -declare void @runtime.trackPointer(i8* nocapture readonly, i8*, i8*) - -; Function Attrs: nounwind -define hidden void @main.init(i8* %context, i8* %parentHandle) unnamed_addr #0 { -entry: - ret void -} - -; Function Attrs: nounwind -define hidden void @main.regularFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 { -entry: - call void @"internal/task.start"(i32 ptrtoint (void (i32, i8*, i8*)* @main.regularFunction to i32), i8* bitcast ({ i32, i8* }* @"main$pack" to i8*), i32 undef, i8* undef, i8* null) #0 - ret void -} - -declare void @main.regularFunction(i32, i8*, i8*) - -declare void @"internal/task.start"(i32, i8*, i32, i8*, i8*) - -; Function Attrs: nounwind -define hidden void @main.inlineFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 { -entry: - call void @"internal/task.start"(i32 ptrtoint (void (i32, i8*, i8*)* @"main.inlineFunctionGoroutine$1" to i32), i8* bitcast ({ i32, i8* }* @"main$pack.1" to i8*), i32 undef, i8* undef, i8* null) #0 - ret void -} - -; Function Attrs: nounwind -define hidden void @"main.inlineFunctionGoroutine$1"(i32 %x, i8* %context, i8* %parentHandle) unnamed_addr #0 { -entry: - ret void -} - -; Function Attrs: nounwind -define hidden void @main.closureFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 { -entry: - %n = call i8* @runtime.alloc(i32 4, i8* nonnull inttoptr (i32 3 to i8*), i8* undef, i8* null) #0 - %0 = bitcast i8* %n to i32* - call void @runtime.trackPointer(i8* nonnull %n, i8* undef, i8* null) #0 - store i32 3, i32* %0, align 4 - call void @runtime.trackPointer(i8* nonnull %n, i8* undef, i8* null) #0 - %1 = call i8* @runtime.alloc(i32 8, i8* null, i8* undef, i8* null) #0 - call void @runtime.trackPointer(i8* nonnull %1, i8* undef, i8* null) #0 - %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) #0 - %5 = load i32, i32* %0, align 4 - call void @runtime.printint32(i32 %5, i8* undef, i8* null) #0 - ret void -} - -; Function Attrs: nounwind -define hidden void @"main.closureFunctionGoroutine$1"(i32 %x, i8* %context, i8* %parentHandle) unnamed_addr #0 { -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*) - -; Function Attrs: nounwind -define hidden void @main.funcGoroutine(i8* %fn.context, i32 %fn.funcptr, i8* %context, i8* %parentHandle) unnamed_addr #0 { -entry: - %0 = call i32 @runtime.getFuncPtr(i8* %fn.context, i32 %fn.funcptr, i8* nonnull @"reflect/types.funcid:func:{basic:int}{}", i8* undef, i8* null) #0 - %1 = call i8* @runtime.alloc(i32 8, i8* null, i8* undef, i8* null) #0 - call void @runtime.trackPointer(i8* nonnull %1, i8* undef, i8* null) #0 - %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) #0 - ret void -} - -declare i32 @runtime.getFuncPtr(i8*, i32, i8* dereferenceable_or_null(1), i8*, i8*) - -; Function Attrs: nounwind -define hidden void @main.recoverBuiltinGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 { -entry: - ret void -} - -; Function Attrs: nounwind -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 #0 { -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) #0 - ret void -} - -declare i32 @runtime.sliceCopy(i8* nocapture writeonly, i8* nocapture readonly, i32, i32, i32, i8*, i8*) - -; Function Attrs: nounwind -define hidden void @main.closeBuiltinGoroutine(%runtime.channel* dereferenceable_or_null(32) %ch, i8* %context, i8* %parentHandle) unnamed_addr #0 { -entry: - call void @runtime.chanClose(%runtime.channel* %ch, i8* undef, i8* null) #0 - ret void -} - -declare void @runtime.chanClose(%runtime.channel* dereferenceable_or_null(32), i8*, i8*) - -; Function Attrs: nounwind -define hidden void @main.startInterfaceMethod(i32 %itf.typecode, i8* %itf.value, i8* %context, i8* %parentHandle) unnamed_addr #0 { -entry: - %0 = call i8* @runtime.alloc(i32 16, i8* null, i8* undef, i8* null) #0 - call void @runtime.trackPointer(i8* nonnull %0, i8* undef, i8* null) #0 - %1 = bitcast i8* %0 to i8** - store i8* %itf.value, i8** %1, align 4 - %2 = getelementptr inbounds i8, i8* %0, i32 4 - %.repack = bitcast i8* %2 to i8** - store i8* getelementptr inbounds ([4 x i8], [4 x i8]* @"main$string", i32 0, i32 0), i8** %.repack, align 4 - %.repack1 = getelementptr inbounds i8, i8* %0, i32 8 - %3 = bitcast i8* %.repack1 to i32* - store i32 4, i32* %3, align 4 - %4 = getelementptr inbounds i8, i8* %0, i32 12 - %5 = bitcast i8* %4 to i32* - store i32 %itf.typecode, i32* %5, align 4 - call void @"internal/task.start"(i32 ptrtoint (void (i8*, i8*, i32, i32, i8*, i8*)* @"interface:{Print:func:{basic:string}{}}.Print$invoke" to i32), i8* nonnull %0, i32 undef, i8* undef, i8* null) #0 - ret void -} - -declare void @"interface:{Print:func:{basic:string}{}}.Print$invoke"(i8*, i8*, i32, i32, i8*, i8*) #1 - -attributes #0 = { nounwind } -attributes #1 = { "tinygo-invoke"="reflect/methods.Print(string)" "tinygo-methods"="reflect/methods.Print(string)" } diff --git a/main.go b/main.go index 91ba96f6..a23a2618 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,6 @@ import ( "github.com/tinygo-org/tinygo/goenv" "github.com/tinygo-org/tinygo/interp" "github.com/tinygo-org/tinygo/loader" - "github.com/tinygo-org/tinygo/transform" "tinygo.org/x/go-llvm" "go.bug.st/serial" @@ -1100,15 +1099,6 @@ func printCompilerError(logln func(...interface{}), err error) { logln() } } - case transform.CoroutinesError: - logln(err.Pos.String() + ": " + err.Msg) - logln("\ntraceback:") - for _, line := range err.Traceback { - logln(line.Name) - if line.Position.IsValid() { - logln("\t" + line.Position.String()) - } - } case loader.Errors: logln("#", err.Pkg.ImportPath) for _, err := range err.Errs { @@ -1195,7 +1185,7 @@ func main() { opt := flag.String("opt", "z", "optimization level: 0, 1, 2, s, z") gc := flag.String("gc", "", "garbage collector to use (none, leaking, conservative)") panicStrategy := flag.String("panic", "print", "panic strategy (print, trap)") - scheduler := flag.String("scheduler", "", "which scheduler to use (none, coroutines, tasks, asyncify)") + scheduler := flag.String("scheduler", "", "which scheduler to use (none, tasks, asyncify)") serial := flag.String("serial", "", "which serial output to use (none, uart, usb)") printIR := flag.Bool("printir", false, "print LLVM IR") dumpSSA := flag.Bool("dumpssa", false, "dump internal Go SSA") diff --git a/src/internal/task/gc_stack_chain.go b/src/internal/task/gc_stack_chain.go index 4c8be170..07588d40 100644 --- a/src/internal/task/gc_stack_chain.go +++ b/src/internal/task/gc_stack_chain.go @@ -1,5 +1,5 @@ -//go:build gc.conservative && tinygo.wasm && !scheduler.coroutines -// +build gc.conservative,tinygo.wasm,!scheduler.coroutines +//go:build gc.conservative && tinygo.wasm +// +build gc.conservative,tinygo.wasm package task diff --git a/src/internal/task/gc_stack_noop.go b/src/internal/task/gc_stack_noop.go index 270a5a4d..b1cfd448 100644 --- a/src/internal/task/gc_stack_noop.go +++ b/src/internal/task/gc_stack_noop.go @@ -1,5 +1,5 @@ -//go:build !gc.conservative || !tinygo.wasm || scheduler.coroutines -// +build !gc.conservative !tinygo.wasm scheduler.coroutines +//go:build !gc.conservative || !tinygo.wasm +// +build !gc.conservative !tinygo.wasm package task diff --git a/src/internal/task/task_coroutine.go b/src/internal/task/task_coroutine.go deleted file mode 100644 index 3a267cb9..00000000 --- a/src/internal/task/task_coroutine.go +++ /dev/null @@ -1,90 +0,0 @@ -// +build scheduler.coroutines - -package task - -import ( - "unsafe" -) - -// rawState is an underlying coroutine state exposed by llvm.coro. -// This matches *i8 in LLVM. -type rawState uint8 - -//export llvm.coro.resume -func coroResume(*rawState) - -type state struct{ *rawState } - -//export llvm.coro.noop -func noopState() *rawState - -// Resume the task until it pauses or completes. -func (t *Task) Resume() { - coroResume(t.state.rawState) -} - -// setState is used by the compiler to set the state of the function at the beginning of a function call. -// Returns the state of the caller. -func (t *Task) setState(s *rawState) *rawState { - caller := t.state - t.state = state{s} - return caller.rawState -} - -// returnTo is used by the compiler to return to the state of the caller. -func (t *Task) returnTo(parent *rawState) { - t.state = state{parent} - t.returnCurrent() -} - -// returnCurrent is used by the compiler to return to the state of the caller in a case where the state is not replaced. -func (t *Task) returnCurrent() { - scheduleTask(t) -} - -//go:linkname scheduleTask runtime.runqueuePushBack -func scheduleTask(*Task) - -// setReturnPtr is used by the compiler to store the return buffer into the task. -// This buffer is where the return value of a function that is about to be called will be stored. -func (t *Task) setReturnPtr(buf unsafe.Pointer) { - t.Ptr = buf -} - -// getReturnPtr is used by the compiler to get the return buffer stored into the task. -// This is called at the beginning of an async function, and the return is stored into this buffer immediately before resuming the caller. -func (t *Task) getReturnPtr() unsafe.Pointer { - return t.Ptr -} - -// createTask returns a new task struct initialized with a no-op state. -func createTask() *Task { - return &Task{ - state: state{noopState()}, - } -} - -// start invokes a function in a new goroutine. Calls to this are inserted by the compiler. -// The created goroutine starts running immediately. -// This is implemented inside the compiler. -func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) - -// Current returns the current active task. -// This is implemented inside the compiler. -func Current() *Task - -// Pause suspends the current running task. -// This is implemented inside the compiler. -func Pause() - -func fake() { - // Hack to ensure intrinsics are discovered. - Current() - Pause() -} - -// OnSystemStack returns whether the caller is running on the system stack. -func OnSystemStack() bool { - // This scheduler does not do any stack switching. - return true -} diff --git a/src/runtime/chan.go b/src/runtime/chan.go index a5229f0e..9b13a983 100644 --- a/src/runtime/chan.go +++ b/src/runtime/chan.go @@ -15,13 +15,13 @@ package runtime // closed: // The channel is closed. Sends will panic, receives will get a zero value // plus optionally the indication that the channel is zero (with the -// commao-ok value in the coroutine). +// comma-ok value in the task). // // A send/recv transmission is completed by copying from the data element of the -// sending coroutine to the data element of the receiving coroutine, and setting +// sending task to the data element of the receiving task, and setting // the 'comma-ok' value to true. // A receive operation on a closed channel is completed by zeroing the data -// element of the receiving coroutine and setting the 'comma-ok' value to false. +// element of the receiving task and setting the 'comma-ok' value to false. import ( "internal/task" diff --git a/src/runtime/scheduler.go b/src/runtime/scheduler.go index 618b6638..196f6e3a 100644 --- a/src/runtime/scheduler.go +++ b/src/runtime/scheduler.go @@ -6,13 +6,9 @@ package runtime // were added to the queue (first-in, first-out). It also contains a sleep queue // with sleeping goroutines in order of when they should be re-activated. // -// The scheduler is used both for the coroutine based scheduler and for the task -// based scheduler (see compiler/goroutine-lowering.go for a description). In -// both cases, the 'task' type is used to represent one goroutine. In the case -// of the task based scheduler, it literally is the goroutine itself: a pointer -// to the bottom of the stack where some important fields are kept. In the case -// of the coroutine-based scheduler, it is the coroutine pointer (a *i8 in -// LLVM). +// The scheduler is used both for the asyncify based scheduler and for the task +// based scheduler. In both cases, the 'internal/task.Task' type is used to represent one +// goroutine. import ( "internal/task" diff --git a/src/runtime/scheduler_coroutines.go b/src/runtime/scheduler_coroutines.go deleted file mode 100644 index 5e871a64..00000000 --- a/src/runtime/scheduler_coroutines.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build scheduler.coroutines - -package runtime - -// getSystemStackPointer returns the current stack pointer of the system stack. -// This is always the current stack pointer. -func getSystemStackPointer() uintptr { - return getCurrentStackPointer() -} diff --git a/testdata/goroutines.go b/testdata/goroutines.go index 128bdb4a..66abc54f 100644 --- a/testdata/goroutines.go +++ b/testdata/goroutines.go @@ -20,7 +20,7 @@ func main() { time.Sleep(2 * time.Millisecond) println("main 3") - // Await a blocking call. This must create a new coroutine. + // Await a blocking call. println("wait:") wait() println("end waiting") diff --git a/transform/coroutines.go b/transform/coroutines.go deleted file mode 100644 index b737fec4..00000000 --- a/transform/coroutines.go +++ /dev/null @@ -1,1106 +0,0 @@ -package transform - -// This file lowers asynchronous functions and goroutine starts when using the coroutines scheduler. -// This is accomplished by inserting LLVM intrinsics which are used in order to save the states of functions. - -import ( - "errors" - "go/token" - "strconv" - - "github.com/tinygo-org/tinygo/compiler/llvmutil" - "tinygo.org/x/go-llvm" -) - -// LowerCoroutines turns async functions into coroutines. -// This must be run with the coroutines scheduler. -// -// Before this pass, goroutine starts are expressed as a call to an intrinsic called "internal/task.start". -// This intrinsic accepts the function pointer and a pointer to a struct containing the function's arguments. -// -// Before this pass, an intrinsic called "internal/task.Pause" is used to express suspensions of the current goroutine. -// -// This pass first accumulates a list of blocking functions. -// A function is considered "blocking" if it calls "internal/task.Pause" or any other blocking function. -// -// Blocking calls are implemented by turning blocking functions into a coroutine. -// The body of each blocking function is modified to start a new coroutine, and to return after the first suspend. -// After calling a blocking function, the caller coroutine suspends. -// The caller also provides a buffer to store the return value into. -// When a blocking function returns, the return value is written into this buffer and then the caller is queued to run. -// -// Goroutine starts which invoke non-blocking functions are implemented as direct calls. -// Goroutine starts are replaced with the creation of a new task data structure followed by a call to the start of the blocking function. -// The task structure is populated with a "noop" coroutine before invoking the blocking function. -// When the blocking function returns, it resumes this "noop" coroutine which does nothing. -// The goroutine starter is able to continue after the first suspend of the started goroutine. -// -// The transformation of a function to a coroutine is accomplished using LLVM's coroutines system (https://llvm.org/docs/Coroutines.html). -// The simplest implementation of a coroutine inserts a suspend point after every blocking call. -// -// Transforming blocking functions into coroutines and calls into suspend points is extremely expensive. -// In many cases, a blocking call is followed immediately by a function terminator (a return or an "unreachable" instruction). -// This is a blocking "tail call". -// In a non-returning tail call (call to a non-returning function, such as an infinite loop), the coroutine can exit without any extra work. -// In a returning tail call, the returned value must either be the return of the call or a value known before the call. -// If the return value of the caller is the return of the callee, the coroutine can exit without any extra work and the tailed call will instead return to the caller of the caller. -// If the return value is known in advance, this result can be stored into the parent's return buffer before the call so that a suspend is unnecessary. -// If the callee returns an unnecessary value, a return buffer can be allocated on the heap so that it will outlive the coroutine. -// -// In the implementation of time.Sleep, the current task is pushed onto a timer queue and then suspended. -// Since the only suspend point is a call to "internal/task.Pause" followed by a return, there is no need to transform this into a coroutine. -// This generalizes to all blocking functions in which all suspend points can be elided. -// This optimization saves a substantial amount of binary size. -func LowerCoroutines(mod llvm.Module, needStackSlots bool) error { - ctx := mod.Context() - - builder := ctx.NewBuilder() - defer builder.Dispose() - - target := llvm.NewTargetData(mod.DataLayout()) - defer target.Dispose() - - pass := &coroutineLoweringPass{ - mod: mod, - ctx: ctx, - builder: builder, - target: target, - needStackSlots: needStackSlots, - } - - err := pass.load() - if err != nil { - return err - } - - // Supply task operands to async calls. - pass.supplyTaskOperands() - - // Analyze async returns. - pass.returnAnalysisPass() - - // Categorize async calls. - pass.categorizeCalls() - - // Lower async functions. - pass.lowerFuncsPass() - - // Lower calls to internal/task.Current. - pass.lowerCurrent() - - // Lower goroutine starts. - pass.lowerStartsPass() - - // Fix annotations on async call params. - pass.fixAnnotations() - - if needStackSlots { - // Set up garbage collector tracking of tasks at start. - err = pass.trackGoroutines() - if err != nil { - return err - } - } - - return nil -} - -// CoroutinesError is an error returned when coroutine lowering failed, for -// example because an async function is exported. -type CoroutinesError struct { - Msg string - Pos token.Position - Traceback []CoroutinesErrorLine -} - -// CoroutinesErrorLine is a single line of a CoroutinesError traceback. -type CoroutinesErrorLine struct { - Name string // function name - Position token.Position // position in the function -} - -// Error implements the error interface by returning a simple error message -// without the stack. -func (err CoroutinesError) Error() string { - return err.Msg -} - -type asyncCallInfo struct { - fn llvm.Value - call llvm.Value -} - -// asyncFunc is a metadata container for an asynchronous function. -type asyncFunc struct { - // fn is the underlying function pointer. - fn llvm.Value - - // rawTask is the parameter where the task pointer is passed in. - rawTask llvm.Value - - // callers is a set of all functions which call this async function. - callers map[llvm.Value]struct{} - - // returns is a list of returns in the function, along with metadata. - returns []asyncReturn - - // calls is a list of all calls in the asyncFunc. - // normalCalls is a list of all intermideate suspending calls in the asyncFunc. - // tailCalls is a list of all tail calls in the asyncFunc. - calls, normalCalls, tailCalls []llvm.Value -} - -// asyncReturn is a metadata container for a return from an asynchronous function. -type asyncReturn struct { - // block is the basic block terminated by the return. - block llvm.BasicBlock - - // kind is the kind of the return. - kind returnKind -} - -// coroutineLoweringPass is a goroutine lowering pass which is used with the "coroutines" scheduler. -type coroutineLoweringPass struct { - mod llvm.Module - ctx llvm.Context - builder llvm.Builder - target llvm.TargetData - - // asyncFuncs is a map of all asyncFuncs. - // The map keys are function pointers. - asyncFuncs map[llvm.Value]*asyncFunc - - asyncFuncsOrdered []*asyncFunc - - // calls is a slice of all of the async calls in the module. - calls []llvm.Value - - i8ptr llvm.Type - - // memory management functions from the runtime - alloc, free llvm.Value - - // coroutine intrinsics - start, pause, current llvm.Value - setState, setRetPtr, getRetPtr, returnTo, returnCurrent llvm.Value - createTask llvm.Value - - // llvm.coro intrinsics - coroId, coroSize, coroBegin, coroSuspend, coroEnd, coroFree, coroSave llvm.Value - - trackPointer llvm.Value - needStackSlots bool -} - -// findAsyncFuncs finds all asynchronous functions. -// A function is considered asynchronous if it calls an asynchronous function or intrinsic. -func (c *coroutineLoweringPass) findAsyncFuncs() error { - asyncs := map[llvm.Value]*asyncFunc{} - asyncsOrdered := []llvm.Value{} - calls := []llvm.Value{} - callsAsyncFunction := map[llvm.Value]asyncCallInfo{} - - // Use a breadth-first search to find all async functions. - worklist := []llvm.Value{c.pause} - for len(worklist) > 0 { - // Pop a function off the worklist. - fn := worklist[0] - worklist = worklist[1:] - - // Get task pointer argument. - task := fn.LastParam() - if fn != c.pause && (task.IsNil() || task.Name() != "parentHandle") { - // Exported functions must not do async operations. - err := CoroutinesError{ - Msg: "blocking operation in exported function: " + fn.Name(), - Pos: getPosition(fn), - } - f := fn - for !f.IsNil() && f != c.pause { - data := callsAsyncFunction[f] - err.Traceback = append(err.Traceback, CoroutinesErrorLine{f.Name(), getPosition(data.call)}) - f = data.fn - } - return err - } - - // Search all uses of the function while collecting callers. - callers := map[llvm.Value]struct{}{} - for use := fn.FirstUse(); !use.IsNil(); use = use.NextUse() { - user := use.User() - if user.IsACallInst().IsNil() { - // User is not a call instruction, so this is irrelevant. - continue - } - if user.CalledValue() != fn { - // Not the called value. - continue - } - - // Add to calls list. - calls = append(calls, user) - - // Get the caller. - caller := user.InstructionParent().Parent() - - // Add as caller. - callers[caller] = struct{}{} - - if _, ok := asyncs[caller]; ok { - // Already marked caller as async. - continue - } - - // Mark the caller as async. - // Use nil as a temporary value. It will be replaced later. - asyncs[caller] = nil - asyncsOrdered = append(asyncsOrdered, caller) - - // Track which calls caused this function to be marked async (for - // better diagnostics). - callsAsyncFunction[caller] = asyncCallInfo{ - fn: fn, - call: user, - } - - // Put the caller on the worklist. - worklist = append(worklist, caller) - } - - asyncs[fn] = &asyncFunc{ - fn: fn, - rawTask: task, - callers: callers, - } - } - - // Flip the order of the async functions so that the top ones are lowered first. - for i := 0; i < len(asyncsOrdered)/2; i++ { - asyncsOrdered[i], asyncsOrdered[len(asyncsOrdered)-(i+1)] = asyncsOrdered[len(asyncsOrdered)-(i+1)], asyncsOrdered[i] - } - - // Map the elements of asyncsOrdered to *asyncFunc. - asyncFuncsOrdered := make([]*asyncFunc, len(asyncsOrdered)) - for i, v := range asyncsOrdered { - asyncFuncsOrdered[i] = asyncs[v] - } - - c.asyncFuncs = asyncs - c.asyncFuncsOrdered = asyncFuncsOrdered - c.calls = calls - - return nil -} - -func (c *coroutineLoweringPass) load() error { - // Find memory management functions from the runtime. - c.alloc = c.mod.NamedFunction("runtime.alloc") - if c.alloc.IsNil() { - return ErrMissingIntrinsic{"runtime.alloc"} - } - c.free = c.mod.NamedFunction("runtime.free") - if c.free.IsNil() { - return ErrMissingIntrinsic{"runtime.free"} - } - - // Find intrinsics. - c.pause = c.mod.NamedFunction("internal/task.Pause") - if c.pause.IsNil() { - return ErrMissingIntrinsic{"internal/task.Pause"} - } - c.start = c.mod.NamedFunction("internal/task.start") - if c.start.IsNil() { - return ErrMissingIntrinsic{"internal/task.start"} - } - c.current = c.mod.NamedFunction("internal/task.Current") - if c.current.IsNil() { - return ErrMissingIntrinsic{"internal/task.Current"} - } - c.setState = c.mod.NamedFunction("(*internal/task.Task).setState") - if c.setState.IsNil() { - return ErrMissingIntrinsic{"(*internal/task.Task).setState"} - } - c.setRetPtr = c.mod.NamedFunction("(*internal/task.Task).setReturnPtr") - if c.setRetPtr.IsNil() { - return ErrMissingIntrinsic{"(*internal/task.Task).setReturnPtr"} - } - c.getRetPtr = c.mod.NamedFunction("(*internal/task.Task).getReturnPtr") - if c.getRetPtr.IsNil() { - return ErrMissingIntrinsic{"(*internal/task.Task).getReturnPtr"} - } - c.returnTo = c.mod.NamedFunction("(*internal/task.Task).returnTo") - if c.returnTo.IsNil() { - return ErrMissingIntrinsic{"(*internal/task.Task).returnTo"} - } - c.returnCurrent = c.mod.NamedFunction("(*internal/task.Task).returnCurrent") - if c.returnCurrent.IsNil() { - return ErrMissingIntrinsic{"(*internal/task.Task).returnCurrent"} - } - c.createTask = c.mod.NamedFunction("internal/task.createTask") - if c.createTask.IsNil() { - return ErrMissingIntrinsic{"internal/task.createTask"} - } - - if c.needStackSlots { - c.trackPointer = c.mod.NamedFunction("runtime.trackPointer") - if c.trackPointer.IsNil() { - return ErrMissingIntrinsic{"runtime.trackPointer"} - } - } - - // Find async functions. - err := c.findAsyncFuncs() - if err != nil { - return err - } - - // Get i8* type. - c.i8ptr = llvm.PointerType(c.ctx.Int8Type(), 0) - - // Build LLVM coroutine intrinsic. - coroIdType := llvm.FunctionType(c.ctx.TokenType(), []llvm.Type{c.ctx.Int32Type(), c.i8ptr, c.i8ptr, c.i8ptr}, false) - c.coroId = llvm.AddFunction(c.mod, "llvm.coro.id", coroIdType) - - sizeT := c.alloc.Param(0).Type() - coroSizeType := llvm.FunctionType(sizeT, nil, false) - c.coroSize = llvm.AddFunction(c.mod, "llvm.coro.size.i"+strconv.Itoa(sizeT.IntTypeWidth()), coroSizeType) - - coroBeginType := llvm.FunctionType(c.i8ptr, []llvm.Type{c.ctx.TokenType(), c.i8ptr}, false) - c.coroBegin = llvm.AddFunction(c.mod, "llvm.coro.begin", coroBeginType) - - coroSuspendType := llvm.FunctionType(c.ctx.Int8Type(), []llvm.Type{c.ctx.TokenType(), c.ctx.Int1Type()}, false) - c.coroSuspend = llvm.AddFunction(c.mod, "llvm.coro.suspend", coroSuspendType) - - coroEndType := llvm.FunctionType(c.ctx.Int1Type(), []llvm.Type{c.i8ptr, c.ctx.Int1Type()}, false) - c.coroEnd = llvm.AddFunction(c.mod, "llvm.coro.end", coroEndType) - - coroFreeType := llvm.FunctionType(c.i8ptr, []llvm.Type{c.ctx.TokenType(), c.i8ptr}, false) - c.coroFree = llvm.AddFunction(c.mod, "llvm.coro.free", coroFreeType) - - coroSaveType := llvm.FunctionType(c.ctx.TokenType(), []llvm.Type{c.i8ptr}, false) - c.coroSave = llvm.AddFunction(c.mod, "llvm.coro.save", coroSaveType) - - return nil -} - -func (c *coroutineLoweringPass) track(ptr llvm.Value) { - if c.needStackSlots { - if ptr.Type() != c.i8ptr { - ptr = c.builder.CreateBitCast(ptr, c.i8ptr, "track.bitcast") - } - c.builder.CreateCall(c.trackPointer, []llvm.Value{ptr, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "") - } -} - -// lowerStartSync lowers a goroutine start of a synchronous function to a synchronous call. -func (c *coroutineLoweringPass) lowerStartSync(start llvm.Value) { - c.builder.SetInsertPointBefore(start) - - // Get function to call. - fn := start.Operand(0).Operand(0) - - // Create the list of params for the call. - paramTypes := fn.Type().ElementType().ParamTypes() - params := llvmutil.EmitPointerUnpack(c.builder, c.mod, start.Operand(1), paramTypes[:len(paramTypes)-1]) - params = append(params, llvm.Undef(c.i8ptr)) - - // Generate call to function. - c.builder.CreateCall(fn, params, "") - - // Remove start call. - start.EraseFromParentAsInstruction() -} - -// supplyTaskOperands fills in the task operands of async calls. -func (c *coroutineLoweringPass) supplyTaskOperands() { - var curCalls []llvm.Value - for use := c.current.FirstUse(); !use.IsNil(); use = use.NextUse() { - curCalls = append(curCalls, use.User()) - } - for _, call := range append(curCalls, c.calls...) { - c.builder.SetInsertPointBefore(call) - task := c.asyncFuncs[call.InstructionParent().Parent()].rawTask - call.SetOperand(call.OperandsCount()-2, task) - } -} - -// returnKind is a classification of a type of function terminator. -type returnKind uint8 - -const ( - // returnNormal is a terminator that returns a value normally from a function. - returnNormal returnKind = iota - - // returnVoid is a terminator that exits normally without returning a value. - returnVoid - - // returnVoidTail is a terminator which is a tail call to a void-returning function in a void-returning function. - returnVoidTail - - // returnTail is a terinator which is a tail call to a value-returning function where the value is returned by the callee. - returnTail - - // returnDeadTail is a terminator which is a call to a non-returning asynchronous function. - returnDeadTail - - // returnAlternateTail is a terminator which is a tail call to a value-returning function where a previously acquired value is returned by the callee. - returnAlternateTail - - // returnDitchedTail is a terminator which is a tail call to a value-returning function, where the callee returns void. - returnDitchedTail - - // returnDelayedValue is a terminator in which a void-returning tail call is followed by a return of a previous value. - returnDelayedValue -) - -// isAsyncCall returns whether the specified call is async. -func (c *coroutineLoweringPass) isAsyncCall(call llvm.Value) bool { - _, ok := c.asyncFuncs[call.CalledValue()] - return ok -} - -// analyzeFuncReturns analyzes and classifies the returns of a function. -func (c *coroutineLoweringPass) analyzeFuncReturns(fn *asyncFunc) { - returns := []asyncReturn{} - if fn.fn == c.pause { - // Skip pause. - fn.returns = returns - return - } - - for _, bb := range fn.fn.BasicBlocks() { - last := bb.LastInstruction() - switch last.InstructionOpcode() { - case llvm.Ret: - // Check if it is a void return. - isVoid := fn.fn.Type().ElementType().ReturnType().TypeKind() == llvm.VoidTypeKind - - // Analyze previous instruction. - prev := llvm.PrevInstruction(last) - switch { - case prev.IsNil(): - fallthrough - case prev.IsACallInst().IsNil(): - fallthrough - case !c.isAsyncCall(prev): - // This is not any form of asynchronous tail call. - if isVoid { - returns = append(returns, asyncReturn{ - block: bb, - kind: returnVoid, - }) - } else { - returns = append(returns, asyncReturn{ - block: bb, - kind: returnNormal, - }) - } - case isVoid: - if prev.CalledValue().Type().ElementType().ReturnType().TypeKind() == llvm.VoidTypeKind { - // This is a tail call to a void-returning function from a function with a void return. - returns = append(returns, asyncReturn{ - block: bb, - kind: returnVoidTail, - }) - } else { - // This is a tail call to a value-returning function from a function with a void return. - // The returned value will be ditched. - returns = append(returns, asyncReturn{ - block: bb, - kind: returnDitchedTail, - }) - } - case last.Operand(0) == prev: - // This is a regular tail call. The return of the callee is returned to the parent. - returns = append(returns, asyncReturn{ - block: bb, - kind: returnTail, - }) - case prev.CalledValue().Type().ElementType().ReturnType().TypeKind() == llvm.VoidTypeKind: - // This is a tail call that returns a previous value after waiting on a void function. - returns = append(returns, asyncReturn{ - block: bb, - kind: returnDelayedValue, - }) - default: - // This is a tail call that returns a value that is available before the function call. - returns = append(returns, asyncReturn{ - block: bb, - kind: returnAlternateTail, - }) - } - case llvm.Unreachable: - prev := llvm.PrevInstruction(last) - - if prev.IsNil() || prev.IsACallInst().IsNil() || !c.isAsyncCall(prev) { - // This unreachable instruction does not behave as an asynchronous return. - continue - } - - // This is an asyncnhronous tail call to function that does not return. - returns = append(returns, asyncReturn{ - block: bb, - kind: returnDeadTail, - }) - } - } - - fn.returns = returns -} - -// returnAnalysisPass runs an analysis pass which classifies the returns of all async functions. -func (c *coroutineLoweringPass) returnAnalysisPass() { - for _, async := range c.asyncFuncsOrdered { - c.analyzeFuncReturns(async) - } -} - -// categorizeCalls categorizes all asynchronous calls into regular vs. async and matches them to their callers. -func (c *coroutineLoweringPass) categorizeCalls() { - // Sort calls into their respective callers. - for _, call := range c.calls { - caller := c.asyncFuncs[call.InstructionParent().Parent()] - caller.calls = append(caller.calls, call) - } - - // Seperate regular and tail calls. - for _, async := range c.asyncFuncsOrdered { - // Search returns for tail calls. - tails := map[llvm.Value]struct{}{} - for _, ret := range async.returns { - switch ret.kind { - case returnVoidTail, returnTail, returnDeadTail, returnAlternateTail, returnDitchedTail, returnDelayedValue: - // This is a tail return. The previous instruction is a tail call. - tails[llvm.PrevInstruction(ret.block.LastInstruction())] = struct{}{} - } - } - - // Seperate tail calls and regular calls. - normalCalls, tailCalls := []llvm.Value{}, []llvm.Value{} - for _, call := range async.calls { - if _, ok := tails[call]; ok { - // This is a tail call. - tailCalls = append(tailCalls, call) - } else { - // This is a regular call. - normalCalls = append(normalCalls, call) - } - } - - async.normalCalls = normalCalls - async.tailCalls = tailCalls - } -} - -// lowerFuncsPass lowers all functions, turning them into coroutines if necessary. -func (c *coroutineLoweringPass) lowerFuncsPass() { - for _, fn := range c.asyncFuncs { - if fn.fn == c.pause { - // Skip. It is an intrinsic. - continue - } - - if len(fn.normalCalls) == 0 && fn.fn.FirstBasicBlock().FirstInstruction().IsAAllocaInst().IsNil() { - // No suspend points or stack allocations. Lower without turning it into a coroutine. - c.lowerFuncFast(fn) - } else { - // There are suspend points or stack allocations, so it is necessary to turn this into a coroutine. - c.lowerFuncCoro(fn) - } - } -} - -func (async *asyncFunc) hasValueStoreReturn() bool { - for _, ret := range async.returns { - switch ret.kind { - case returnNormal, returnAlternateTail, returnDelayedValue: - return true - } - } - - return false -} - -// heapAlloc creates a heap allocation large enough to hold the supplied type. -// The allocation is returned as a raw i8* pointer. -// This allocation is not automatically tracked by the garbage collector, and should thus be stored into a tracked memory object immediately. -func (c *coroutineLoweringPass) heapAlloc(t llvm.Type, name string) llvm.Value { - sizeT := c.alloc.FirstParam().Type() - size := llvm.ConstInt(sizeT, c.target.TypeAllocSize(t), false) - return c.builder.CreateCall(c.alloc, []llvm.Value{size, llvm.ConstNull(c.i8ptr), llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, name) -} - -// lowerFuncFast lowers an async function that has no suspend points. -func (c *coroutineLoweringPass) lowerFuncFast(fn *asyncFunc) { - // Get return type. - retType := fn.fn.Type().ElementType().ReturnType() - - // Get task value. - c.insertPointAfterAllocas(fn.fn) - task := c.builder.CreateCall(c.current, []llvm.Value{llvm.Undef(c.i8ptr), fn.rawTask}, "task") - - // Get return pointer if applicable. - var rawRetPtr, retPtr llvm.Value - if fn.hasValueStoreReturn() { - rawRetPtr = c.builder.CreateCall(c.getRetPtr, []llvm.Value{task, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "ret.ptr") - retType = fn.fn.Type().ElementType().ReturnType() - retPtr = c.builder.CreateBitCast(rawRetPtr, llvm.PointerType(retType, 0), "ret.ptr.bitcast") - } - - // Lower returns. - for _, ret := range fn.returns { - // Get terminator. - terminator := ret.block.LastInstruction() - - // Get tail call if applicable. - var call llvm.Value - switch ret.kind { - case returnVoidTail, returnTail, returnDeadTail, returnAlternateTail, returnDitchedTail, returnDelayedValue: - call = llvm.PrevInstruction(terminator) - } - - switch ret.kind { - case returnNormal: - c.builder.SetInsertPointBefore(terminator) - - // Store value into return pointer. - c.builder.CreateStore(terminator.Operand(0), retPtr) - - // Resume caller. - c.builder.CreateCall(c.returnCurrent, []llvm.Value{task, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "") - - // Erase return argument. - terminator.SetOperand(0, llvm.Undef(retType)) - case returnVoid: - c.builder.SetInsertPointBefore(terminator) - - // Resume caller. - c.builder.CreateCall(c.returnCurrent, []llvm.Value{task, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "") - case returnVoidTail: - // Nothing to do. There is already a tail call followed by a void return. - case returnTail: - // Erase return argument. - terminator.SetOperand(0, llvm.Undef(retType)) - case returnDeadTail: - // Replace unreachable with immediate return, without resuming the caller. - c.builder.SetInsertPointBefore(terminator) - if retType.TypeKind() == llvm.VoidTypeKind { - c.builder.CreateRetVoid() - } else { - c.builder.CreateRet(llvm.Undef(retType)) - } - terminator.EraseFromParentAsInstruction() - case returnAlternateTail: - c.builder.SetInsertPointBefore(call) - - // Store return value. - c.builder.CreateStore(terminator.Operand(0), retPtr) - - // Heap-allocate a return buffer for the discarded return. - alternateBuf := c.heapAlloc(call.Type(), "ret.alternate") - c.builder.CreateCall(c.setRetPtr, []llvm.Value{task, alternateBuf, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "") - - // Erase return argument. - terminator.SetOperand(0, llvm.Undef(retType)) - case returnDitchedTail: - c.builder.SetInsertPointBefore(call) - - // Heap-allocate a return buffer for the discarded return. - ditchBuf := c.heapAlloc(call.Type(), "ret.ditch") - c.builder.CreateCall(c.setRetPtr, []llvm.Value{task, ditchBuf, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "") - case returnDelayedValue: - c.builder.SetInsertPointBefore(call) - - // Store value into return pointer. - c.builder.CreateStore(terminator.Operand(0), retPtr) - - // Erase return argument. - terminator.SetOperand(0, llvm.Undef(retType)) - } - - // Delete call if it is a pause, because it has already been lowered. - if !call.IsNil() && call.CalledValue() == c.pause { - call.EraseFromParentAsInstruction() - } - } -} - -// insertPointAfterAllocas sets the insert point of the builder to be immediately after the last alloca in the entry block. -func (c *coroutineLoweringPass) insertPointAfterAllocas(fn llvm.Value) { - inst := fn.EntryBasicBlock().FirstInstruction() - for !inst.IsAAllocaInst().IsNil() { - inst = llvm.NextInstruction(inst) - } - c.builder.SetInsertPointBefore(inst) -} - -// lowerCallReturn lowers the return value of an async call by creating a return buffer and loading the returned value from it. -func (c *coroutineLoweringPass) lowerCallReturn(caller *asyncFunc, call llvm.Value) { - // Get return type. - retType := call.Type() - if retType.TypeKind() == llvm.VoidTypeKind { - // Void return. Nothing to do. - return - } - - // Create alloca for return buffer. - alloca := llvmutil.CreateInstructionAlloca(c.builder, c.mod, retType, call, "call.return") - - // Store new return buffer into task before call. - c.builder.SetInsertPointBefore(call) - task := c.builder.CreateCall(c.current, []llvm.Value{llvm.Undef(c.i8ptr), caller.rawTask}, "call.task") - retPtr := c.builder.CreateBitCast(alloca, c.i8ptr, "call.return.bitcast") - c.builder.CreateCall(c.setRetPtr, []llvm.Value{task, retPtr, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "") - - // Load return value after call. - c.builder.SetInsertPointBefore(llvm.NextInstruction(call)) - ret := c.builder.CreateLoad(alloca, "call.return.load") - - // Replace call value with loaded return. - call.ReplaceAllUsesWith(ret) -} - -// lowerFuncCoro transforms an async function into a coroutine by lowering async operations to `llvm.coro` intrinsics. -// See https://llvm.org/docs/Coroutines.html for more information on these intrinsics. -func (c *coroutineLoweringPass) lowerFuncCoro(fn *asyncFunc) { - // Ensure that any alloca instructions in the entry block are at the start. - // Otherwise, block splitting would result in unintended behavior. - { - // Skip alloca instructions at the start of the block. - inst := fn.fn.FirstBasicBlock().FirstInstruction() - for !inst.IsAAllocaInst().IsNil() { - inst = llvm.NextInstruction(inst) - } - - // Find any other alloca instructions and move them after the other allocas. - c.builder.SetInsertPointBefore(inst) - for !inst.IsNil() { - next := llvm.NextInstruction(inst) - if !inst.IsAAllocaInst().IsNil() { - inst.RemoveFromParentAsInstruction() - c.builder.Insert(inst) - } - inst = next - } - } - - returnType := fn.fn.Type().ElementType().ReturnType() - - // Prepare coroutine state. - c.insertPointAfterAllocas(fn.fn) - // %coro.id = call token @llvm.coro.id(i32 0, i8* null, i8* null, i8* null) - coroId := c.builder.CreateCall(c.coroId, []llvm.Value{ - llvm.ConstInt(c.ctx.Int32Type(), 0, false), - llvm.ConstNull(c.i8ptr), - llvm.ConstNull(c.i8ptr), - llvm.ConstNull(c.i8ptr), - }, "coro.id") - // %coro.size = call i32 @llvm.coro.size.i32() - coroSize := c.builder.CreateCall(c.coroSize, []llvm.Value{}, "coro.size") - // %coro.alloc = call i8* runtime.alloc(i32 %coro.size) - coroAlloc := c.builder.CreateCall(c.alloc, []llvm.Value{coroSize, llvm.ConstNull(c.i8ptr), llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "coro.alloc") - // %coro.state = call noalias i8* @llvm.coro.begin(token %coro.id, i8* %coro.alloc) - coroState := c.builder.CreateCall(c.coroBegin, []llvm.Value{coroId, coroAlloc}, "coro.state") - c.track(coroState) - // Store state into task. - task := c.builder.CreateCall(c.current, []llvm.Value{llvm.Undef(c.i8ptr), fn.rawTask}, "task") - parentState := c.builder.CreateCall(c.setState, []llvm.Value{task, coroState, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "task.state.parent") - // Get return pointer if needed. - var retPtrRaw, retPtr llvm.Value - if returnType.TypeKind() != llvm.VoidTypeKind { - retPtrRaw = c.builder.CreateCall(c.getRetPtr, []llvm.Value{task, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "task.retPtr") - retPtr = c.builder.CreateBitCast(retPtrRaw, llvm.PointerType(fn.fn.Type().ElementType().ReturnType(), 0), "task.retPtr.bitcast") - } - - // Build suspend block. - // This is executed when the coroutine is about to suspend. - suspend := c.ctx.AddBasicBlock(fn.fn, "suspend") - c.builder.SetInsertPointAtEnd(suspend) - // %unused = call i1 @llvm.coro.end(i8* %coro.state, i1 false) - c.builder.CreateCall(c.coroEnd, []llvm.Value{coroState, llvm.ConstInt(c.ctx.Int1Type(), 0, false)}, "unused") - // Insert return. - if returnType.TypeKind() == llvm.VoidTypeKind { - c.builder.CreateRetVoid() - } else { - c.builder.CreateRet(llvm.Undef(returnType)) - } - - // Build cleanup block. - // This is executed before the function returns in order to clean up resources. - cleanup := c.ctx.AddBasicBlock(fn.fn, "cleanup") - c.builder.SetInsertPointAtEnd(cleanup) - // %coro.memFree = call i8* @llvm.coro.free(token %coro.id, i8* %coro.state) - coroMemFree := c.builder.CreateCall(c.coroFree, []llvm.Value{coroId, coroState}, "coro.memFree") - // call i8* runtime.free(i8* %coro.memFree) - c.builder.CreateCall(c.free, []llvm.Value{coroMemFree, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "") - // Branch to suspend block. - c.builder.CreateBr(suspend) - - // Restore old state before tail calls. - for _, call := range fn.tailCalls { - if !llvm.NextInstruction(call).IsAUnreachableInst().IsNil() { - // Callee never returns, so the state restore is ineffectual. - continue - } - - c.builder.SetInsertPointBefore(call) - c.builder.CreateCall(c.setState, []llvm.Value{task, parentState, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "coro.state.restore") - } - - // Lower returns. - var postTail llvm.BasicBlock - for _, ret := range fn.returns { - // Get terminator instruction. - terminator := ret.block.LastInstruction() - - // Get tail call if applicable. - var call llvm.Value - switch ret.kind { - case returnVoidTail, returnTail, returnDeadTail, returnAlternateTail, returnDitchedTail, returnDelayedValue: - call = llvm.PrevInstruction(terminator) - } - - switch ret.kind { - case returnNormal: - c.builder.SetInsertPointBefore(terminator) - - // Store value into return pointer. - c.builder.CreateStore(terminator.Operand(0), retPtr) - - // Resume caller. - c.builder.CreateCall(c.returnTo, []llvm.Value{task, parentState, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "") - case returnVoid: - c.builder.SetInsertPointBefore(terminator) - - // Resume caller. - c.builder.CreateCall(c.returnTo, []llvm.Value{task, parentState, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "") - case returnVoidTail, returnDeadTail: - // Nothing to do. - case returnTail: - c.builder.SetInsertPointBefore(call) - - // Restore the return pointer so that the caller can store into it. - c.builder.CreateCall(c.setRetPtr, []llvm.Value{task, retPtrRaw, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "") - case returnAlternateTail: - c.builder.SetInsertPointBefore(call) - - // Store return value. - c.builder.CreateStore(terminator.Operand(0), retPtr) - - // Heap-allocate a return buffer for the discarded return. - alternateBuf := c.heapAlloc(call.Type(), "ret.alternate") - c.builder.CreateCall(c.setRetPtr, []llvm.Value{task, alternateBuf, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "") - case returnDitchedTail: - c.builder.SetInsertPointBefore(call) - - // Heap-allocate a return buffer for the discarded return. - ditchBuf := c.heapAlloc(call.Type(), "ret.ditch") - c.builder.CreateCall(c.setRetPtr, []llvm.Value{task, ditchBuf, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "") - case returnDelayedValue: - c.builder.SetInsertPointBefore(call) - - // Store return value. - c.builder.CreateStore(terminator.Operand(0), retPtr) - } - - // Delete call if it is a pause, because it has already been lowered. - if !call.IsNil() && call.CalledValue() == c.pause { - call.EraseFromParentAsInstruction() - } - - // Replace terminator with a branch to the exit. - var exit llvm.BasicBlock - if ret.kind == returnNormal || ret.kind == returnVoid || fn.fn.FirstBasicBlock().FirstInstruction().IsAAllocaInst().IsNil() { - // Exit through the cleanup path. - exit = cleanup - } else { - if postTail.IsNil() { - // Create a path with a suspend that never reawakens. - postTail = c.ctx.AddBasicBlock(fn.fn, "post.tail") - c.builder.SetInsertPointAtEnd(postTail) - // %coro.save = call token @llvm.coro.save(i8* %coro.state) - save := c.builder.CreateCall(c.coroSave, []llvm.Value{coroState}, "coro.save") - // %call.suspend = llvm.coro.suspend(token %coro.save, i1 false) - // switch i8 %call.suspend, label %suspend [i8 0, label %wakeup - // i8 1, label %cleanup] - suspendValue := c.builder.CreateCall(c.coroSuspend, []llvm.Value{save, llvm.ConstInt(c.ctx.Int1Type(), 0, false)}, "call.suspend") - sw := c.builder.CreateSwitch(suspendValue, suspend, 2) - unreachableBlock := c.ctx.AddBasicBlock(fn.fn, "unreachable") - sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), unreachableBlock) - sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), cleanup) - c.builder.SetInsertPointAtEnd(unreachableBlock) - c.builder.CreateUnreachable() - } - - // Exit through a permanent suspend. - exit = postTail - } - - terminator.EraseFromParentAsInstruction() - c.builder.SetInsertPointAtEnd(ret.block) - c.builder.CreateBr(exit) - } - - // Lower regular calls. - for _, call := range fn.normalCalls { - // Lower return value of call. - c.lowerCallReturn(fn, call) - - // Get originating basic block. - bb := call.InstructionParent() - - // Split block. - wakeup := llvmutil.SplitBasicBlock(c.builder, call, llvm.NextBasicBlock(bb), "wakeup") - - // Insert suspension and switch. - c.builder.SetInsertPointAtEnd(bb) - // %coro.save = call token @llvm.coro.save(i8* %coro.state) - save := c.builder.CreateCall(c.coroSave, []llvm.Value{coroState}, "coro.save") - // %call.suspend = llvm.coro.suspend(token %coro.save, i1 false) - // switch i8 %call.suspend, label %suspend [i8 0, label %wakeup - // i8 1, label %cleanup] - suspendValue := c.builder.CreateCall(c.coroSuspend, []llvm.Value{save, llvm.ConstInt(c.ctx.Int1Type(), 0, false)}, "call.suspend") - sw := c.builder.CreateSwitch(suspendValue, suspend, 2) - sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), wakeup) - sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), cleanup) - - // Delete call if it is a pause, because it has already been lowered. - if call.CalledValue() == c.pause { - call.EraseFromParentAsInstruction() - } - - c.builder.SetInsertPointBefore(wakeup.FirstInstruction()) - c.track(coroState) - } -} - -// lowerCurrent lowers calls to internal/task.Current to bitcasts. -func (c *coroutineLoweringPass) lowerCurrent() error { - taskType := c.current.Type().ElementType().ReturnType() - deleteQueue := []llvm.Value{} - for use := c.current.FirstUse(); !use.IsNil(); use = use.NextUse() { - // Get user. - user := use.User() - - if user.IsACallInst().IsNil() || user.CalledValue() != c.current { - return errorAt(user, "unexpected non-call use of task.Current") - } - - // Replace with bitcast. - c.builder.SetInsertPointBefore(user) - raw := user.Operand(1) - if !raw.IsAUndefValue().IsNil() || raw.IsNull() { - return errors.New("undefined task") - } - task := c.builder.CreateBitCast(raw, taskType, "task.current") - user.ReplaceAllUsesWith(task) - deleteQueue = append(deleteQueue, user) - } - - // Delete calls. - for _, inst := range deleteQueue { - inst.EraseFromParentAsInstruction() - } - - return nil -} - -// lowerStart lowers a goroutine start into a task creation and call or a synchronous call. -func (c *coroutineLoweringPass) lowerStart(start llvm.Value) { - c.builder.SetInsertPointBefore(start) - - // Get function to call. - fn := start.Operand(0).Operand(0) - - if _, ok := c.asyncFuncs[fn]; !ok { - // Turn into synchronous call. - c.lowerStartSync(start) - return - } - - // Create the list of params for the call. - paramTypes := fn.Type().ElementType().ParamTypes() - params := llvmutil.EmitPointerUnpack(c.builder, c.mod, start.Operand(1), paramTypes[:len(paramTypes)-1]) - - // Create task. - task := c.builder.CreateCall(c.createTask, []llvm.Value{llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "start.task") - rawTask := c.builder.CreateBitCast(task, c.i8ptr, "start.task.bitcast") - params = append(params, rawTask) - - // Generate a return buffer if necessary. - returnType := fn.Type().ElementType().ReturnType() - if returnType.TypeKind() == llvm.VoidTypeKind { - // No return buffer necessary for a void return. - } else { - // Check for any undead returns. - var undead bool - for _, ret := range c.asyncFuncs[fn].returns { - if ret.kind != returnDeadTail { - // This return results in a value being eventually stored. - undead = true - break - } - } - if undead { - // The function stores a value into a return buffer, so we need to create one. - retBuf := c.heapAlloc(returnType, "ret.ditch") - c.builder.CreateCall(c.setRetPtr, []llvm.Value{task, retBuf, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "") - } - } - - // Generate call to function. - c.builder.CreateCall(fn, params, "") - - // Erase start call. - start.EraseFromParentAsInstruction() -} - -// lowerStartsPass lowers all goroutine starts. -func (c *coroutineLoweringPass) lowerStartsPass() { - starts := []llvm.Value{} - for use := c.start.FirstUse(); !use.IsNil(); use = use.NextUse() { - starts = append(starts, use.User()) - } - for _, start := range starts { - c.lowerStart(start) - } -} - -func (c *coroutineLoweringPass) fixAnnotations() { - for f := range c.asyncFuncs { - // These properties were added by the functionattrs pass. Remove - // them, because now we start using the parameter. - // https://llvm.org/docs/Passes.html#functionattrs-deduce-function-attributes - for _, kind := range []string{"nocapture", "readnone"} { - kindID := llvm.AttributeKindID(kind) - n := f.ParamsCount() - for i := 0; i <= n; i++ { - f.RemoveEnumAttributeAtIndex(i, kindID) - } - } - } -} - -// trackGoroutines adds runtime.trackPointer calls to track goroutine starts and data. -func (c *coroutineLoweringPass) trackGoroutines() error { - trackPointer := c.mod.NamedFunction("runtime.trackPointer") - if trackPointer.IsNil() { - return ErrMissingIntrinsic{"runtime.trackPointer"} - } - - trackFunctions := []llvm.Value{c.createTask, c.setState, c.getRetPtr} - for _, fn := range trackFunctions { - for use := fn.FirstUse(); !use.IsNil(); use = use.NextUse() { - call := use.User() - - c.builder.SetInsertPointBefore(llvm.NextInstruction(call)) - ptr := call - if ptr.Type() != c.i8ptr { - ptr = c.builder.CreateBitCast(call, c.i8ptr, "") - } - c.builder.CreateCall(trackPointer, []llvm.Value{ptr, llvm.Undef(c.i8ptr), llvm.Undef(c.i8ptr)}, "") - } - } - - return nil -} diff --git a/transform/goroutine_test.go b/transform/goroutine_test.go deleted file mode 100644 index 4d4f0b19..00000000 --- a/transform/goroutine_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package transform_test - -import ( - "testing" - - "github.com/tinygo-org/tinygo/transform" - "tinygo.org/x/go-llvm" -) - -func TestGoroutineLowering(t *testing.T) { - t.Parallel() - testTransform(t, "testdata/coroutines", func(mod llvm.Module) { - err := transform.LowerCoroutines(mod, false) - if err != nil { - panic(err) - } - }) -} diff --git a/transform/optimizer.go b/transform/optimizer.go index cd79340c..ab90ca7c 100644 --- a/transform/optimizer.go +++ b/transform/optimizer.go @@ -29,10 +29,9 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i if inlinerThreshold != 0 { builder.UseInlinerWithThreshold(inlinerThreshold) } - builder.AddCoroutinePassesToExtensionPoints() // Make sure these functions are kept in tact during TinyGo transformation passes. - for _, name := range getFunctionsUsedInTransforms(config) { + for _, name := range functionsUsedInTransforms { fn := mod.NamedFunction(name) if fn.IsNil() { panic(fmt.Errorf("missing core function %q", name)) @@ -118,17 +117,7 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i goPasses.Run(mod) } - // Lower async implementations. - switch config.Scheduler() { - case "coroutines": - // Lower async as coroutines. - err := LowerCoroutines(mod, config.NeedsStackObjects()) - if err != nil { - return []error{err} - } - case "tasks", "asyncify": - // No transformations necessary. - case "none": + if config.Scheduler() == "none" { // Check for any goroutine starts. if start := mod.NamedFunction("internal/task.start"); !start.IsNil() && len(getUses(start)) > 0 { errs := []error{} @@ -137,8 +126,6 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i } return errs } - default: - return []error{errors.New("invalid scheduler")} } if config.VerifyIR() { @@ -151,7 +138,7 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i } // After TinyGo-specific transforms have finished, undo exporting these functions. - for _, name := range getFunctionsUsedInTransforms(config) { + for _, name := range functionsUsedInTransforms { fn := mod.NamedFunction(name) if fn.IsNil() || fn.IsDeclaration() { continue @@ -195,35 +182,3 @@ var functionsUsedInTransforms = []string{ "runtime.free", "runtime.nilPanic", } - -var taskFunctionsUsedInTransforms = []string{} - -// These functions need to be preserved in the IR until after the coroutines -// pass has run. -var coroFunctionsUsedInTransforms = []string{ - "internal/task.start", - "internal/task.Pause", - "internal/task.fake", - "internal/task.Current", - "internal/task.createTask", - "(*internal/task.Task).setState", - "(*internal/task.Task).returnTo", - "(*internal/task.Task).returnCurrent", - "(*internal/task.Task).setReturnPtr", - "(*internal/task.Task).getReturnPtr", -} - -// getFunctionsUsedInTransforms gets a list of all special functions that should be preserved during transforms and optimization. -func getFunctionsUsedInTransforms(config *compileopts.Config) []string { - fnused := functionsUsedInTransforms - switch config.Scheduler() { - case "none": - case "coroutines": - fnused = append(append([]string{}, fnused...), coroFunctionsUsedInTransforms...) - case "tasks", "asyncify": - fnused = append(append([]string{}, fnused...), taskFunctionsUsedInTransforms...) - default: - panic(fmt.Errorf("invalid scheduler %q", config.Scheduler())) - } - return fnused -} diff --git a/transform/testdata/coroutines.ll b/transform/testdata/coroutines.ll deleted file mode 100644 index c5f711d2..00000000 --- a/transform/testdata/coroutines.ll +++ /dev/null @@ -1,152 +0,0 @@ -target datalayout = "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64" -target triple = "armv7m-none-eabi" - -%"internal/task.state" = type { i8* } -%"internal/task.Task" = type { %"internal/task.Task", i8*, i32, %"internal/task.state" } - -declare void @"internal/task.start"(i32, i8*, i32, i8*, i8*) -declare void @"internal/task.Pause"(i8*, i8*) - -declare void @runtime.scheduler(i8*, i8*) - -declare i8* @runtime.alloc(i32, i8*, i8*, i8*) -declare void @runtime.free(i8*, i8*, i8*) - -declare %"internal/task.Task"* @"internal/task.Current"(i8*, i8*) - -declare i8* @"(*internal/task.Task).setState"(%"internal/task.Task"*, i8*, i8*, i8*) -declare void @"(*internal/task.Task).setReturnPtr"(%"internal/task.Task"*, i8*, i8*, i8*) -declare i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"*, i8*, i8*) -declare void @"(*internal/task.Task).returnTo"(%"internal/task.Task"*, i8*, i8*, i8*) -declare void @"(*internal/task.Task).returnCurrent"(%"internal/task.Task"*, i8*, i8*) -declare %"internal/task.Task"* @"internal/task.createTask"(i8*, i8*) - -declare void @callMain(i8*, i8*) - -; Test a simple sleep-like scenario. -declare void @enqueueTimer(%"internal/task.Task"*, i64, i8*, i8*) - -define void @sleep(i64, i8*, i8* %parentHandle) { -entry: - %2 = call %"internal/task.Task"* @"internal/task.Current"(i8* undef, i8* null) - call void @enqueueTimer(%"internal/task.Task"* %2, i64 %0, i8* undef, i8* null) - call void @"internal/task.Pause"(i8* undef, i8* null) - ret void -} - -; Test a delayed value return. -define i32 @delayedValue(i32, i64, i8*, i8* %parentHandle) { -entry: - call void @sleep(i64 %1, i8* undef, i8* null) - ret i32 %0 -} - -; Test a deadlocking async func. -define void @deadlock(i8*, i8* %parentHandle) { -entry: - call void @"internal/task.Pause"(i8* undef, i8* null) - unreachable -} - -; Test a regular tail call. -define i32 @tail(i32, i64, i8*, i8* %parentHandle) { -entry: - %3 = call i32 @delayedValue(i32 %0, i64 %1, i8* undef, i8* null) - ret i32 %3 -} - -; Test a ditching tail call. -define void @ditchTail(i32, i64, i8*, i8* %parentHandle) { -entry: - %3 = call i32 @delayedValue(i32 %0, i64 %1, i8* undef, i8* null) - ret void -} - -; Test a void tail call. -define void @voidTail(i32, i64, i8*, i8* %parentHandle) { -entry: - call void @ditchTail(i32 %0, i64 %1, i8* undef, i8* null) - ret void -} - -; Test a tail call returning an alternate value. -define i32 @alternateTail(i32, i32, i64, i8*, i8* %parentHandle) { -entry: - %4 = call i32 @delayedValue(i32 %1, i64 %2, i8* undef, i8* null) - ret i32 %0 -} - -; Test a normal return from a coroutine. -; This must be turned into a coroutine. -define i1 @coroutine(i32, i64, i8*, i8* %parentHandle) { -entry: - %3 = call i32 @delayedValue(i32 %0, i64 %1, i8* undef, i8* null) - %4 = icmp eq i32 %3, 0 - ret i1 %4 -} - -; Normal function which should not be transformed. -define void @doNothing(i8*, i8* %parentHandle) { -entry: - ret void -} - -; Regression test: ensure that a tail call does not destroy the frame while it is still in use. -; Previously, the tail-call lowering transform would branch to the cleanup block after usePtr. -; This caused the lifetime of %a to be incorrectly reduced, and allowed the coroutine lowering transform to keep %a on the stack. -; After a suspend %a would be used, resulting in memory corruption. -define i8 @coroutineTailRegression(i8*, i8* %parentHandle) { -entry: - %a = alloca i8 - store i8 5, i8* %a - %val = call i8 @usePtr(i8* %a, i8* undef, i8* null) - ret i8 %val -} - -; Regression test: ensure that stack allocations alive during a suspend end up on the heap. -; This used to not be transformed to a coroutine, keeping %a on the stack. -; After a suspend %a would be used, resulting in memory corruption. -define i8 @allocaTailRegression(i8*, i8* %parentHandle) { -entry: - %a = alloca i8 - call void @sleep(i64 1000000, i8* undef, i8* null) - store i8 5, i8* %a - %val = call i8 @usePtr(i8* %a, i8* undef, i8* null) - ret i8 %val -} - -; usePtr uses a pointer after a suspend. -define i8 @usePtr(i8*, i8*, i8* %parentHandle) { -entry: - call void @sleep(i64 1000000, i8* undef, i8* null) - %val = load i8, i8* %0 - ret i8 %val -} - -; Goroutine that sleeps and does nothing. -; Should be a void tail call. -define void @sleepGoroutine(i8*, i8* %parentHandle) { - call void @sleep(i64 1000000, i8* undef, i8* null) - ret void -} - -; Program main function. -define void @progMain(i8*, i8* %parentHandle) { -entry: - ; Call a sync func in a goroutine. - call void @"internal/task.start"(i32 ptrtoint (void (i8*, i8*)* @doNothing to i32), i8* undef, i32 undef, i8* undef, i8* null) - ; Call an async func in a goroutine. - call void @"internal/task.start"(i32 ptrtoint (void (i8*, i8*)* @sleepGoroutine to i32), i8* undef, i32 undef, i8* undef, i8* null) - ; Sleep a bit. - call void @sleep(i64 2000000, i8* undef, i8* null) - ; Done. - ret void -} - -; Entrypoint of runtime. -define void @main() { -entry: - call void @"internal/task.start"(i32 ptrtoint (void (i8*, i8*)* @progMain to i32), i8* undef, i32 undef, i8* undef, i8* null) - call void @runtime.scheduler(i8* undef, i8* null) - ret void -} diff --git a/transform/testdata/coroutines.out.ll b/transform/testdata/coroutines.out.ll deleted file mode 100644 index de902884..00000000 --- a/transform/testdata/coroutines.out.ll +++ /dev/null @@ -1,313 +0,0 @@ -target datalayout = "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64" -target triple = "armv7m-none-eabi" - -%"internal/task.Task" = type { %"internal/task.Task", i8*, i32, %"internal/task.state" } -%"internal/task.state" = type { i8* } - -declare void @"internal/task.start"(i32, i8*, i32, i8*, i8*) - -declare void @"internal/task.Pause"(i8*, i8*) - -declare void @runtime.scheduler(i8*, i8*) - -declare i8* @runtime.alloc(i32, i8*, i8*, i8*) - -declare void @runtime.free(i8*, i8*, i8*) - -declare %"internal/task.Task"* @"internal/task.Current"(i8*, i8*) - -declare i8* @"(*internal/task.Task).setState"(%"internal/task.Task"*, i8*, i8*, i8*) - -declare void @"(*internal/task.Task).setReturnPtr"(%"internal/task.Task"*, i8*, i8*, i8*) - -declare i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"*, i8*, i8*) - -declare void @"(*internal/task.Task).returnTo"(%"internal/task.Task"*, i8*, i8*, i8*) - -declare void @"(*internal/task.Task).returnCurrent"(%"internal/task.Task"*, i8*, i8*) - -declare %"internal/task.Task"* @"internal/task.createTask"(i8*, i8*) - -declare void @callMain(i8*, i8*) - -declare void @enqueueTimer(%"internal/task.Task"*, i64, i8*, i8*) - -define void @sleep(i64 %0, i8* %1, i8* %parentHandle) { -entry: - %task.current = bitcast i8* %parentHandle to %"internal/task.Task"* - %task.current1 = bitcast i8* %parentHandle to %"internal/task.Task"* - call void @enqueueTimer(%"internal/task.Task"* %task.current1, i64 %0, i8* undef, i8* null) - ret void -} - -define i32 @delayedValue(i32 %0, i64 %1, i8* %2, i8* %parentHandle) { -entry: - %task.current = bitcast i8* %parentHandle to %"internal/task.Task"* - %ret.ptr = call i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"* %task.current, i8* undef, i8* undef) - %ret.ptr.bitcast = bitcast i8* %ret.ptr to i32* - store i32 %0, i32* %ret.ptr.bitcast, align 4 - call void @sleep(i64 %1, i8* undef, i8* %parentHandle) - ret i32 undef -} - -define void @deadlock(i8* %0, i8* %parentHandle) { -entry: - %task.current = bitcast i8* %parentHandle to %"internal/task.Task"* - ret void -} - -define i32 @tail(i32 %0, i64 %1, i8* %2, i8* %parentHandle) { -entry: - %task.current = bitcast i8* %parentHandle to %"internal/task.Task"* - %3 = call i32 @delayedValue(i32 %0, i64 %1, i8* undef, i8* %parentHandle) - ret i32 undef -} - -define void @ditchTail(i32 %0, i64 %1, i8* %2, i8* %parentHandle) { -entry: - %task.current = bitcast i8* %parentHandle to %"internal/task.Task"* - %ret.ditch = call i8* @runtime.alloc(i32 4, i8* null, i8* undef, i8* undef) - call void @"(*internal/task.Task).setReturnPtr"(%"internal/task.Task"* %task.current, i8* %ret.ditch, i8* undef, i8* undef) - %3 = call i32 @delayedValue(i32 %0, i64 %1, i8* undef, i8* %parentHandle) - ret void -} - -define void @voidTail(i32 %0, i64 %1, i8* %2, i8* %parentHandle) { -entry: - %task.current = bitcast i8* %parentHandle to %"internal/task.Task"* - call void @ditchTail(i32 %0, i64 %1, i8* undef, i8* %parentHandle) - ret void -} - -define i32 @alternateTail(i32 %0, i32 %1, i64 %2, i8* %3, i8* %parentHandle) { -entry: - %task.current = bitcast i8* %parentHandle to %"internal/task.Task"* - %ret.ptr = call i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"* %task.current, i8* undef, i8* undef) - %ret.ptr.bitcast = bitcast i8* %ret.ptr to i32* - store i32 %0, i32* %ret.ptr.bitcast, align 4 - %ret.alternate = call i8* @runtime.alloc(i32 4, i8* null, i8* undef, i8* undef) - call void @"(*internal/task.Task).setReturnPtr"(%"internal/task.Task"* %task.current, i8* %ret.alternate, i8* undef, i8* undef) - %4 = call i32 @delayedValue(i32 %1, i64 %2, i8* undef, i8* %parentHandle) - ret i32 undef -} - -define i1 @coroutine(i32 %0, i64 %1, i8* %2, i8* %parentHandle) { -entry: - %call.return = alloca i32, align 4 - %coro.id = call token @llvm.coro.id(i32 0, i8* null, i8* null, i8* null) - %coro.size = call i32 @llvm.coro.size.i32() - %coro.alloc = call i8* @runtime.alloc(i32 %coro.size, i8* null, i8* undef, i8* undef) - %coro.state = call i8* @llvm.coro.begin(token %coro.id, i8* %coro.alloc) - %task.current2 = bitcast i8* %parentHandle to %"internal/task.Task"* - %task.state.parent = call i8* @"(*internal/task.Task).setState"(%"internal/task.Task"* %task.current2, i8* %coro.state, i8* undef, i8* undef) - %task.retPtr = call i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"* %task.current2, i8* undef, i8* undef) - %task.retPtr.bitcast = bitcast i8* %task.retPtr to i1* - %call.return.bitcast = bitcast i32* %call.return to i8* - call void @llvm.lifetime.start.p0i8(i64 4, i8* %call.return.bitcast) - %task.current = bitcast i8* %parentHandle to %"internal/task.Task"* - %call.return.bitcast1 = bitcast i32* %call.return to i8* - call void @"(*internal/task.Task).setReturnPtr"(%"internal/task.Task"* %task.current, i8* %call.return.bitcast1, i8* undef, i8* undef) - %3 = call i32 @delayedValue(i32 %0, i64 %1, i8* undef, i8* %parentHandle) - %coro.save = call token @llvm.coro.save(i8* %coro.state) - %call.suspend = call i8 @llvm.coro.suspend(token %coro.save, i1 false) - switch i8 %call.suspend, label %suspend [ - i8 0, label %wakeup - i8 1, label %cleanup - ] - -wakeup: ; preds = %entry - %4 = load i32, i32* %call.return, align 4 - call void @llvm.lifetime.end.p0i8(i64 4, i8* %call.return.bitcast) - %5 = icmp eq i32 %4, 0 - store i1 %5, i1* %task.retPtr.bitcast, align 1 - call void @"(*internal/task.Task).returnTo"(%"internal/task.Task"* %task.current2, i8* %task.state.parent, i8* undef, i8* undef) - br label %cleanup - -suspend: ; preds = %entry, %cleanup - %unused = call i1 @llvm.coro.end(i8* %coro.state, i1 false) - ret i1 undef - -cleanup: ; preds = %entry, %wakeup - %coro.memFree = call i8* @llvm.coro.free(token %coro.id, i8* %coro.state) - call void @runtime.free(i8* %coro.memFree, i8* undef, i8* undef) - br label %suspend -} - -define void @doNothing(i8* %0, i8* %parentHandle) { -entry: - ret void -} - -define i8 @coroutineTailRegression(i8* %0, i8* %parentHandle) { -entry: - %a = alloca i8, align 1 - %coro.id = call token @llvm.coro.id(i32 0, i8* null, i8* null, i8* null) - %coro.size = call i32 @llvm.coro.size.i32() - %coro.alloc = call i8* @runtime.alloc(i32 %coro.size, i8* null, i8* undef, i8* undef) - %coro.state = call i8* @llvm.coro.begin(token %coro.id, i8* %coro.alloc) - %task.current = bitcast i8* %parentHandle to %"internal/task.Task"* - %task.state.parent = call i8* @"(*internal/task.Task).setState"(%"internal/task.Task"* %task.current, i8* %coro.state, i8* undef, i8* undef) - %task.retPtr = call i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"* %task.current, i8* undef, i8* undef) - store i8 5, i8* %a, align 1 - %coro.state.restore = call i8* @"(*internal/task.Task).setState"(%"internal/task.Task"* %task.current, i8* %task.state.parent, i8* undef, i8* undef) - call void @"(*internal/task.Task).setReturnPtr"(%"internal/task.Task"* %task.current, i8* %task.retPtr, i8* undef, i8* undef) - %val = call i8 @usePtr(i8* %a, i8* undef, i8* %parentHandle) - br label %post.tail - -suspend: ; preds = %post.tail, %cleanup - %unused = call i1 @llvm.coro.end(i8* %coro.state, i1 false) - ret i8 undef - -cleanup: ; preds = %post.tail - %coro.memFree = call i8* @llvm.coro.free(token %coro.id, i8* %coro.state) - call void @runtime.free(i8* %coro.memFree, i8* undef, i8* undef) - br label %suspend - -post.tail: ; preds = %entry - %coro.save = call token @llvm.coro.save(i8* %coro.state) - %call.suspend = call i8 @llvm.coro.suspend(token %coro.save, i1 false) - switch i8 %call.suspend, label %suspend [ - i8 0, label %unreachable - i8 1, label %cleanup - ] - -unreachable: ; preds = %post.tail - unreachable -} - -define i8 @allocaTailRegression(i8* %0, i8* %parentHandle) { -entry: - %a = alloca i8, align 1 - %coro.id = call token @llvm.coro.id(i32 0, i8* null, i8* null, i8* null) - %coro.size = call i32 @llvm.coro.size.i32() - %coro.alloc = call i8* @runtime.alloc(i32 %coro.size, i8* null, i8* undef, i8* undef) - %coro.state = call i8* @llvm.coro.begin(token %coro.id, i8* %coro.alloc) - %task.current = bitcast i8* %parentHandle to %"internal/task.Task"* - %task.state.parent = call i8* @"(*internal/task.Task).setState"(%"internal/task.Task"* %task.current, i8* %coro.state, i8* undef, i8* undef) - %task.retPtr = call i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"* %task.current, i8* undef, i8* undef) - call void @sleep(i64 1000000, i8* undef, i8* %parentHandle) - %coro.save1 = call token @llvm.coro.save(i8* %coro.state) - %call.suspend2 = call i8 @llvm.coro.suspend(token %coro.save1, i1 false) - switch i8 %call.suspend2, label %suspend [ - i8 0, label %wakeup - i8 1, label %cleanup - ] - -wakeup: ; preds = %entry - store i8 5, i8* %a, align 1 - %1 = call i8* @"(*internal/task.Task).setState"(%"internal/task.Task"* %task.current, i8* %task.state.parent, i8* undef, i8* undef) - call void @"(*internal/task.Task).setReturnPtr"(%"internal/task.Task"* %task.current, i8* %task.retPtr, i8* undef, i8* undef) - %2 = call i8 @usePtr(i8* %a, i8* undef, i8* %parentHandle) - br label %post.tail - -suspend: ; preds = %entry, %post.tail, %cleanup - %unused = call i1 @llvm.coro.end(i8* %coro.state, i1 false) - ret i8 undef - -cleanup: ; preds = %entry, %post.tail - %coro.memFree = call i8* @llvm.coro.free(token %coro.id, i8* %coro.state) - call void @runtime.free(i8* %coro.memFree, i8* undef, i8* undef) - br label %suspend - -post.tail: ; preds = %wakeup - %coro.save = call token @llvm.coro.save(i8* %coro.state) - %call.suspend = call i8 @llvm.coro.suspend(token %coro.save, i1 false) - switch i8 %call.suspend, label %suspend [ - i8 0, label %unreachable - i8 1, label %cleanup - ] - -unreachable: ; preds = %post.tail - unreachable -} - -define i8 @usePtr(i8* %0, i8* %1, i8* %parentHandle) { -entry: - %coro.id = call token @llvm.coro.id(i32 0, i8* null, i8* null, i8* null) - %coro.size = call i32 @llvm.coro.size.i32() - %coro.alloc = call i8* @runtime.alloc(i32 %coro.size, i8* null, i8* undef, i8* undef) - %coro.state = call i8* @llvm.coro.begin(token %coro.id, i8* %coro.alloc) - %task.current = bitcast i8* %parentHandle to %"internal/task.Task"* - %task.state.parent = call i8* @"(*internal/task.Task).setState"(%"internal/task.Task"* %task.current, i8* %coro.state, i8* undef, i8* undef) - %task.retPtr = call i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"* %task.current, i8* undef, i8* undef) - call void @sleep(i64 1000000, i8* undef, i8* %parentHandle) - %coro.save = call token @llvm.coro.save(i8* %coro.state) - %call.suspend = call i8 @llvm.coro.suspend(token %coro.save, i1 false) - switch i8 %call.suspend, label %suspend [ - i8 0, label %wakeup - i8 1, label %cleanup - ] - -wakeup: ; preds = %entry - %2 = load i8, i8* %0, align 1 - store i8 %2, i8* %task.retPtr, align 1 - call void @"(*internal/task.Task).returnTo"(%"internal/task.Task"* %task.current, i8* %task.state.parent, i8* undef, i8* undef) - br label %cleanup - -suspend: ; preds = %entry, %cleanup - %unused = call i1 @llvm.coro.end(i8* %coro.state, i1 false) - ret i8 undef - -cleanup: ; preds = %entry, %wakeup - %coro.memFree = call i8* @llvm.coro.free(token %coro.id, i8* %coro.state) - call void @runtime.free(i8* %coro.memFree, i8* undef, i8* undef) - br label %suspend -} - -define void @sleepGoroutine(i8* %0, i8* %parentHandle) { - %task.current = bitcast i8* %parentHandle to %"internal/task.Task"* - call void @sleep(i64 1000000, i8* undef, i8* %parentHandle) - ret void -} - -define void @progMain(i8* %0, i8* %parentHandle) { -entry: - %task.current = bitcast i8* %parentHandle to %"internal/task.Task"* - call void @doNothing(i8* undef, i8* undef) - %start.task = call %"internal/task.Task"* @"internal/task.createTask"(i8* undef, i8* undef) - %start.task.bitcast = bitcast %"internal/task.Task"* %start.task to i8* - call void @sleepGoroutine(i8* undef, i8* %start.task.bitcast) - call void @sleep(i64 2000000, i8* undef, i8* %parentHandle) - ret void -} - -define void @main() { -entry: - %start.task = call %"internal/task.Task"* @"internal/task.createTask"(i8* undef, i8* undef) - %start.task.bitcast = bitcast %"internal/task.Task"* %start.task to i8* - call void @progMain(i8* undef, i8* %start.task.bitcast) - call void @runtime.scheduler(i8* undef, i8* null) - ret void -} - -; Function Attrs: argmemonly nounwind readonly -declare token @llvm.coro.id(i32, i8* readnone, i8* nocapture readonly, i8*) #0 - -; Function Attrs: nounwind readnone -declare i32 @llvm.coro.size.i32() #1 - -; Function Attrs: nounwind -declare i8* @llvm.coro.begin(token, i8* writeonly) #2 - -; Function Attrs: nounwind -declare i8 @llvm.coro.suspend(token, i1) #2 - -; Function Attrs: nounwind -declare i1 @llvm.coro.end(i8*, i1) #2 - -; Function Attrs: argmemonly nounwind readonly -declare i8* @llvm.coro.free(token, i8* nocapture readonly) #0 - -; Function Attrs: nounwind -declare token @llvm.coro.save(i8*) #2 - -; Function Attrs: argmemonly nofree nosync nounwind willreturn -declare void @llvm.lifetime.start.p0i8(i64 immarg, i8* nocapture) #3 - -; Function Attrs: argmemonly nofree nosync nounwind willreturn -declare void @llvm.lifetime.end.p0i8(i64 immarg, i8* nocapture) #3 - -attributes #0 = { argmemonly nounwind readonly } -attributes #1 = { nounwind readnone } -attributes #2 = { nounwind } -attributes #3 = { argmemonly nofree nosync nounwind willreturn } diff --git a/transform/wasm-abi.go b/transform/wasm-abi.go index 46639c22..aeba713d 100644 --- a/transform/wasm-abi.go +++ b/transform/wasm-abi.go @@ -36,7 +36,7 @@ func ExternalInt64AsPtr(mod llvm.Module, config *compileopts.Config) error { if strings.HasPrefix(fn.Name(), "llvm.") || strings.HasPrefix(fn.Name(), "runtime.") { // Do not try to modify the signature of internal LLVM functions and // assume that runtime functions are only temporarily exported for - // coroutine lowering. + // transforms. continue } if !fn.GetStringAttributeAtIndex(-1, "tinygo-methods").IsNil() {