compiler: support returning values from async functions

This is implemented as follows:

  * The parent coroutine allocates space for the return value in its
    frame and stores a pointer to this frame in the parent coroutine
    handle.
  * The child coroutine obtains the alloca from its parent using the
    parent coroutine handle. It then stores the result value there.
  * The parent value reads the data from the alloca on resumption.
Этот коммит содержится в:
Ayke van Laethem 2019-04-29 02:08:26 +02:00 коммит произвёл Ron Evans
родитель fb952a722a
коммит 46d5ea8cf6
5 изменённых файлов: 93 добавлений и 42 удалений

Просмотреть файл

@ -347,6 +347,8 @@ func (c *Compiler) Compile(mainPath string) []error {
c.mod.NamedFunction("runtime.chanSend").SetLinkage(llvm.ExternalLinkage) c.mod.NamedFunction("runtime.chanSend").SetLinkage(llvm.ExternalLinkage)
c.mod.NamedFunction("runtime.chanRecv").SetLinkage(llvm.ExternalLinkage) c.mod.NamedFunction("runtime.chanRecv").SetLinkage(llvm.ExternalLinkage)
c.mod.NamedFunction("runtime.sleepTask").SetLinkage(llvm.ExternalLinkage) c.mod.NamedFunction("runtime.sleepTask").SetLinkage(llvm.ExternalLinkage)
c.mod.NamedFunction("runtime.setTaskData").SetLinkage(llvm.ExternalLinkage)
c.mod.NamedFunction("runtime.getTaskData").SetLinkage(llvm.ExternalLinkage)
c.mod.NamedFunction("runtime.activateTask").SetLinkage(llvm.ExternalLinkage) c.mod.NamedFunction("runtime.activateTask").SetLinkage(llvm.ExternalLinkage)
c.mod.NamedFunction("runtime.scheduler").SetLinkage(llvm.ExternalLinkage) c.mod.NamedFunction("runtime.scheduler").SetLinkage(llvm.ExternalLinkage)

Просмотреть файл

@ -10,8 +10,8 @@ package compiler
// go foo() // go foo()
// time.Sleep(2 * time.Second) // time.Sleep(2 * time.Second)
// println("some other operation") // println("some other operation")
// bar() // i := bar()
// println("done") // println("done", *i)
// } // }
// //
// func foo() { // func foo() {
@ -21,9 +21,10 @@ package compiler
// } // }
// } // }
// //
// func bar() { // func bar() *int {
// time.Sleep(time.Second) // time.Sleep(time.Second)
// println("blocking operation completed) // println("blocking operation completed)
// return new(int)
// } // }
// //
// It is transformed by the IR generator in compiler.go into the following // It is transformed by the IR generator in compiler.go into the following
@ -34,8 +35,8 @@ package compiler
// fn() // fn()
// time.Sleep(2 * time.Second) // time.Sleep(2 * time.Second)
// println("some other operation") // println("some other operation")
// bar() // imagine an 'await' keyword in front of this call // i := bar() // imagine an 'await' keyword in front of this call
// println("done") // println("done", *i)
// } // }
// //
// func foo() { // func foo() {
@ -45,9 +46,10 @@ package compiler
// } // }
// } // }
// //
// func bar() { // func bar() *int {
// time.Sleep(time.Second) // time.Sleep(time.Second)
// println("blocking operation completed) // println("blocking operation completed)
// return new(int)
// } // }
// //
// The pass in this file transforms this code even further, to the following // The pass in this file transforms this code even further, to the following
@ -59,9 +61,11 @@ package compiler
// runtime.sleepTask(hdl, 2 * time.Second) // ask the scheduler to re-activate this coroutine at the right time // runtime.sleepTask(hdl, 2 * time.Second) // ask the scheduler to re-activate this coroutine at the right time
// llvm.suspend(hdl) // suspend point // llvm.suspend(hdl) // suspend point
// println("some other operation") // println("some other operation")
// var i *int // allocate space on the stack for the return value
// runtime.setTaskData(hdl, &i) // store return value alloca in our coroutine promise
// bar(hdl) // await, pass a continuation (hdl) to bar // bar(hdl) // await, pass a continuation (hdl) to bar
// llvm.suspend(hdl) // suspend point, wait for the callee to re-activate // llvm.suspend(hdl) // suspend point, wait for the callee to re-activate
// println("done") // println("done", *i)
// runtime.activateTask(parent) // re-activate the parent (nop, there is no parent) // runtime.activateTask(parent) // re-activate the parent (nop, there is no parent)
// } // }
// //
@ -145,7 +149,8 @@ func (c *Compiler) LowerGoroutines() error {
c.mod.NamedFunction("runtime.chanSend").SetLinkage(llvm.InternalLinkage) c.mod.NamedFunction("runtime.chanSend").SetLinkage(llvm.InternalLinkage)
c.mod.NamedFunction("runtime.chanRecv").SetLinkage(llvm.InternalLinkage) c.mod.NamedFunction("runtime.chanRecv").SetLinkage(llvm.InternalLinkage)
c.mod.NamedFunction("runtime.sleepTask").SetLinkage(llvm.InternalLinkage) c.mod.NamedFunction("runtime.sleepTask").SetLinkage(llvm.InternalLinkage)
c.mod.NamedFunction("runtime.activateTask").SetLinkage(llvm.InternalLinkage) c.mod.NamedFunction("runtime.setTaskData").SetLinkage(llvm.InternalLinkage)
c.mod.NamedFunction("runtime.getTaskData").SetLinkage(llvm.InternalLinkage)
c.mod.NamedFunction("runtime.scheduler").SetLinkage(llvm.InternalLinkage) c.mod.NamedFunction("runtime.scheduler").SetLinkage(llvm.InternalLinkage)
return nil return nil
@ -347,10 +352,18 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
// Split this basic block. // Split this basic block.
await := c.splitBasicBlock(inst, llvm.NextBasicBlock(c.builder.GetInsertBlock()), "task.await") await := c.splitBasicBlock(inst, llvm.NextBasicBlock(c.builder.GetInsertBlock()), "task.await")
// Set task state to TASK_STATE_CALL. // Allocate space for the return value.
c.builder.SetInsertPointAtEnd(inst.InstructionParent()) var retvalAlloca llvm.Value
if inst.Type().TypeKind() != llvm.VoidTypeKind {
c.builder.SetInsertPointBefore(inst.InstructionParent().Parent().EntryBasicBlock().FirstInstruction())
retvalAlloca = c.builder.CreateAlloca(inst.Type(), "coro.retvalAlloca")
c.builder.SetInsertPointBefore(inst)
data := c.builder.CreateBitCast(retvalAlloca, c.i8ptrType, "")
c.createRuntimeCall("setTaskData", []llvm.Value{frame.taskHandle, data}, "")
}
// Suspend. // Suspend.
c.builder.SetInsertPointAtEnd(inst.InstructionParent())
continuePoint := c.builder.CreateCall(coroSuspendFunc, []llvm.Value{ continuePoint := c.builder.CreateCall(coroSuspendFunc, []llvm.Value{
llvm.ConstNull(c.ctx.TokenType()), llvm.ConstNull(c.ctx.TokenType()),
llvm.ConstInt(c.ctx.Int1Type(), 0, false), llvm.ConstInt(c.ctx.Int1Type(), 0, false),
@ -358,27 +371,49 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
sw := c.builder.CreateSwitch(continuePoint, frame.suspendBlock, 2) sw := c.builder.CreateSwitch(continuePoint, frame.suspendBlock, 2)
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), await) sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), await)
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), frame.cleanupBlock) sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), frame.cleanupBlock)
if inst.Type().TypeKind() != llvm.VoidTypeKind {
// Load the return value from the alloca. The callee has
// written the return value to it.
c.builder.SetInsertPointBefore(await.FirstInstruction())
retval := c.builder.CreateLoad(retvalAlloca, "coro.retval")
inst.ReplaceAllUsesWith(retval)
}
} }
// Replace return instructions with suspend points that should // Replace return instructions with suspend points that should
// reactivate the parent coroutine. // reactivate the parent coroutine.
for _, inst := range returns { for _, inst := range returns {
if inst.OperandsCount() == 0 { // These properties were added by the functionattrs pass. Remove
// These properties were added by the functionattrs pass. // them, because now we start using the parameter.
// Remove them, because now we start using the parameter.
// https://llvm.org/docs/Passes.html#functionattrs-deduce-function-attributes // https://llvm.org/docs/Passes.html#functionattrs-deduce-function-attributes
for _, kind := range []string{"nocapture", "readnone"} { for _, kind := range []string{"nocapture", "readnone"} {
kindID := llvm.AttributeKindID(kind) kindID := llvm.AttributeKindID(kind)
f.RemoveEnumAttributeAtIndex(f.ParamsCount(), kindID) f.RemoveEnumAttributeAtIndex(f.ParamsCount(), kindID)
} }
// Reactivate the parent coroutine. This adds it back to
// the run queue, so it is started again by the
// scheduler when possible (possibly right after the
// following suspend).
c.builder.SetInsertPointBefore(inst) c.builder.SetInsertPointBefore(inst)
parentHandle := f.LastParam() parentHandle := f.LastParam()
// Store return values.
switch inst.OperandsCount() {
case 0:
// Nothing to return.
case 1:
// Return this value by writing to the pointer stored in the
// parent handle. The parent coroutine has made an alloca that
// we can write to to store our return value.
returnValuePtr := c.createRuntimeCall("getTaskData", []llvm.Value{parentHandle}, "coro.parentData")
alloca := c.builder.CreateBitCast(returnValuePtr, llvm.PointerType(inst.Operand(0).Type(), 0), "coro.parentAlloca")
c.builder.CreateStore(inst.Operand(0), alloca)
default:
panic("unreachable")
}
// Reactivate the parent coroutine. This adds it back to the run
// queue, so it is started again by the scheduler when possible
// (possibly right after the following suspend).
c.createRuntimeCall("activateTask", []llvm.Value{parentHandle}, "") c.createRuntimeCall("activateTask", []llvm.Value{parentHandle}, "")
// Suspend this coroutine. // Suspend this coroutine.
@ -393,9 +428,6 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), frame.unreachableBlock) sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), frame.unreachableBlock)
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), frame.cleanupBlock) sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), frame.cleanupBlock)
inst.EraseFromParentAsInstruction() inst.EraseFromParentAsInstruction()
} else {
panic("todo: return value from coroutine")
}
} }
// Coroutine cleanup. Free resources associated with this coroutine. // Coroutine cleanup. Free resources associated with this coroutine.

Просмотреть файл

@ -107,6 +107,14 @@ func activateTask(task *coroutine) {
runqueuePushBack(task) runqueuePushBack(task)
} }
func setTaskData(task *coroutine, value unsafe.Pointer) {
task.promise().data = uint(uintptr(value))
}
func getTaskData(task *coroutine) unsafe.Pointer {
return unsafe.Pointer(uintptr(task.promise().data))
}
// Add this task to the end of the run queue. May also destroy the task if it's // Add this task to the end of the run queue. May also destroy the task if it's
// done. // done.
func runqueuePushBack(t *coroutine) { func runqueuePushBack(t *coroutine) {

8
testdata/coroutines.go предоставленный
Просмотреть файл

@ -15,6 +15,9 @@ func main() {
wait() wait()
println("end waiting") println("end waiting")
value := delayedValue()
println("value produced after some time:", value)
// Run a non-blocking call in a goroutine. This should be turned into a // Run a non-blocking call in a goroutine. This should be turned into a
// regular call, so should be equivalent to calling nowait() without 'go' // regular call, so should be equivalent to calling nowait() without 'go'
// prefix. // prefix.
@ -39,6 +42,11 @@ func wait() {
println(" wait end") println(" wait end")
} }
func delayedValue() int {
time.Sleep(time.Millisecond)
return 42
}
func nowait() { func nowait() {
println("non-blocking goroutine") println("non-blocking goroutine")
} }

1
testdata/coroutines.txt предоставленный
Просмотреть файл

@ -7,6 +7,7 @@ wait:
wait start wait start
wait end wait end
end waiting end waiting
value produced after some time: 42
non-blocking goroutine non-blocking goroutine
done with non-blocking goroutine done with non-blocking goroutine
async interface method call async interface method call