mirror of
https://github.com/tinygo-org/tinygo.git
synced 2026-08-12 06:53:40 +00:00
Improved blocking (#513)
core: major improvements to blocking, including support for buffered channels.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"golang.org/x/tools/go/ssa"
|
||||
"tinygo.org/x/go-llvm"
|
||||
)
|
||||
@@ -20,6 +21,9 @@ func (c *Compiler) createRuntimeCall(fnName string, args []llvm.Value, name stri
|
||||
panic("trying to call runtime." + fnName)
|
||||
}
|
||||
fn := c.ir.GetFunction(member.(*ssa.Function))
|
||||
if fn.LLVMFn.IsNil() {
|
||||
panic(fmt.Errorf("function %s does not appear in LLVM IR", fnName))
|
||||
}
|
||||
if !fn.IsExported() {
|
||||
args = append(args, llvm.Undef(c.i8ptrType)) // unused context parameter
|
||||
args = append(args, llvm.ConstPointerNull(c.i8ptrType)) // coroutine handle
|
||||
|
||||
+6
-25
@@ -4,32 +4,17 @@ package compiler
|
||||
// or pseudo-operations that are lowered during goroutine lowering.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/types"
|
||||
|
||||
"golang.org/x/tools/go/ssa"
|
||||
"tinygo.org/x/go-llvm"
|
||||
)
|
||||
|
||||
// emitMakeChan returns a new channel value for the given channel type.
|
||||
func (c *Compiler) emitMakeChan(expr *ssa.MakeChan) (llvm.Value, error) {
|
||||
chanType := c.getLLVMType(expr.Type())
|
||||
size := c.targetData.TypeAllocSize(chanType.ElementType())
|
||||
sizeValue := llvm.ConstInt(c.uintptrType, size, false)
|
||||
ptr := c.createRuntimeCall("alloc", []llvm.Value{sizeValue}, "chan.alloc")
|
||||
ptr = c.builder.CreateBitCast(ptr, chanType, "chan")
|
||||
// Set the elementSize field
|
||||
elementSizePtr := c.builder.CreateGEP(ptr, []llvm.Value{
|
||||
llvm.ConstInt(c.ctx.Int32Type(), 0, false),
|
||||
llvm.ConstInt(c.ctx.Int32Type(), 0, false),
|
||||
}, "")
|
||||
func (c *Compiler) emitMakeChan(frame *Frame, expr *ssa.MakeChan) llvm.Value {
|
||||
elementSize := c.targetData.TypeAllocSize(c.getLLVMType(expr.Type().(*types.Chan).Elem()))
|
||||
if elementSize > 0xffff {
|
||||
return ptr, c.makeError(expr.Pos(), fmt.Sprintf("element size is %d bytes, which is bigger than the maximum of %d bytes", elementSize, 0xffff))
|
||||
}
|
||||
elementSizeValue := llvm.ConstInt(c.ctx.Int16Type(), elementSize, false)
|
||||
c.builder.CreateStore(elementSizeValue, elementSizePtr)
|
||||
return ptr, nil
|
||||
elementSizeValue := llvm.ConstInt(c.uintptrType, elementSize, false)
|
||||
bufSize := c.getValue(frame, expr.Size)
|
||||
return c.createRuntimeCall("chanMake", []llvm.Value{elementSizeValue, bufSize}, "")
|
||||
}
|
||||
|
||||
// emitChanSend emits a pseudo chan send operation. It is lowered to the actual
|
||||
@@ -44,8 +29,7 @@ func (c *Compiler) emitChanSend(frame *Frame, instr *ssa.Send) {
|
||||
c.builder.CreateStore(chanValue, valueAlloca)
|
||||
|
||||
// Do the send.
|
||||
coroutine := c.createRuntimeCall("getCoroutine", nil, "")
|
||||
c.createRuntimeCall("chanSend", []llvm.Value{coroutine, ch, valueAllocaCast}, "")
|
||||
c.createRuntimeCall("chanSend", []llvm.Value{ch, valueAllocaCast}, "")
|
||||
|
||||
// End the lifetime of the alloca.
|
||||
// This also works around a bug in CoroSplit, at least in LLVM 8:
|
||||
@@ -63,14 +47,11 @@ func (c *Compiler) emitChanRecv(frame *Frame, unop *ssa.UnOp) llvm.Value {
|
||||
valueAlloca, valueAllocaCast, valueAllocaSize := c.createTemporaryAlloca(valueType, "chan.value")
|
||||
|
||||
// Do the receive.
|
||||
coroutine := c.createRuntimeCall("getCoroutine", nil, "")
|
||||
c.createRuntimeCall("chanRecv", []llvm.Value{coroutine, ch, valueAllocaCast}, "")
|
||||
commaOk := c.createRuntimeCall("chanRecv", []llvm.Value{ch, valueAllocaCast}, "")
|
||||
received := c.builder.CreateLoad(valueAlloca, "chan.received")
|
||||
c.emitLifetimeEnd(valueAllocaCast, valueAllocaSize)
|
||||
|
||||
if unop.CommaOk {
|
||||
commaOk := c.createRuntimeCall("getTaskStateData", []llvm.Value{coroutine}, "chan.commaOk.wide")
|
||||
commaOk = c.builder.CreateTrunc(commaOk, c.ctx.Int1Type(), "chan.commaOk")
|
||||
tuple := llvm.Undef(c.ctx.StructType([]llvm.Type{valueType, c.ctx.Int1Type()}, false))
|
||||
tuple = c.builder.CreateInsertValue(tuple, received, 0, "")
|
||||
tuple = c.builder.CreateInsertValue(tuple, commaOk, 1, "")
|
||||
|
||||
+31
-7
@@ -36,13 +36,23 @@ const tinygoPath = "github.com/tinygo-org/tinygo"
|
||||
var functionsUsedInTransforms = []string{
|
||||
"runtime.alloc",
|
||||
"runtime.free",
|
||||
"runtime.sleepTask",
|
||||
"runtime.sleepCurrentTask",
|
||||
"runtime.scheduler",
|
||||
}
|
||||
|
||||
var taskFunctionsUsedInTransforms = []string{
|
||||
"runtime.startGoroutine",
|
||||
}
|
||||
|
||||
var coroFunctionsUsedInTransforms = []string{
|
||||
"runtime.avrSleep",
|
||||
"runtime.getFakeCoroutine",
|
||||
"runtime.setTaskStatePtr",
|
||||
"runtime.getTaskStatePtr",
|
||||
"runtime.activateTask",
|
||||
"runtime.scheduler",
|
||||
"runtime.startGoroutine",
|
||||
"runtime.noret",
|
||||
"runtime.getParentHandle",
|
||||
"runtime.getCoroutine",
|
||||
"runtime.llvmCoroRefHolder",
|
||||
}
|
||||
|
||||
// Configure the compiler.
|
||||
@@ -201,6 +211,20 @@ func (c *Compiler) selectScheduler() string {
|
||||
return "coroutines"
|
||||
}
|
||||
|
||||
// getFunctionsUsedInTransforms gets a list of all special functions that should be preserved during transforms and optimization.
|
||||
func (c *Compiler) getFunctionsUsedInTransforms() []string {
|
||||
fnused := functionsUsedInTransforms
|
||||
switch c.selectScheduler() {
|
||||
case "coroutines":
|
||||
fnused = append(append([]string{}, fnused...), coroFunctionsUsedInTransforms...)
|
||||
case "tasks":
|
||||
fnused = append(append([]string{}, fnused...), taskFunctionsUsedInTransforms...)
|
||||
default:
|
||||
panic(fmt.Errorf("invalid scheduler %q", c.selectScheduler()))
|
||||
}
|
||||
return fnused
|
||||
}
|
||||
|
||||
// Compile the given package path or .go file path. Return an error when this
|
||||
// fails (in any stage).
|
||||
func (c *Compiler) Compile(mainPath string) []error {
|
||||
@@ -366,10 +390,10 @@ func (c *Compiler) Compile(mainPath string) []error {
|
||||
realMain.SetLinkage(llvm.ExternalLinkage) // keep alive until goroutine lowering
|
||||
|
||||
// Make sure these functions are kept in tact during TinyGo transformation passes.
|
||||
for _, name := range functionsUsedInTransforms {
|
||||
for _, name := range c.getFunctionsUsedInTransforms() {
|
||||
fn := c.mod.NamedFunction(name)
|
||||
if fn.IsNil() {
|
||||
continue
|
||||
panic(fmt.Errorf("missing core function %q", name))
|
||||
}
|
||||
fn.SetLinkage(llvm.ExternalLinkage)
|
||||
}
|
||||
@@ -1618,7 +1642,7 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) {
|
||||
panic("unknown lookup type: " + expr.String())
|
||||
}
|
||||
case *ssa.MakeChan:
|
||||
return c.emitMakeChan(expr)
|
||||
return c.emitMakeChan(frame, expr), nil
|
||||
case *ssa.MakeClosure:
|
||||
return c.parseMakeClosure(frame, expr)
|
||||
case *ssa.MakeInterface:
|
||||
|
||||
+504
-240
@@ -105,16 +105,20 @@ package compiler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"tinygo.org/x/go-llvm"
|
||||
)
|
||||
|
||||
// setting this to true will cause the compiler to spew tons of information about coroutine transformations
|
||||
// this can be useful when debugging coroutine lowering or looking for potential missed optimizations
|
||||
const coroDebug = false
|
||||
|
||||
type asyncFunc struct {
|
||||
taskHandle llvm.Value
|
||||
cleanupBlock llvm.BasicBlock
|
||||
suspendBlock llvm.BasicBlock
|
||||
unreachableBlock llvm.BasicBlock
|
||||
taskHandle llvm.Value
|
||||
cleanupBlock llvm.BasicBlock
|
||||
suspendBlock llvm.BasicBlock
|
||||
}
|
||||
|
||||
// LowerGoroutines performs some IR transformations necessary to support
|
||||
@@ -142,7 +146,7 @@ func (c *Compiler) lowerTasks() error {
|
||||
mainCall := uses[0]
|
||||
|
||||
realMain := c.mod.NamedFunction(c.ir.MainPkg().Pkg.Path() + ".main")
|
||||
if len(getUses(c.mod.NamedFunction("runtime.startGoroutine"))) != 0 {
|
||||
if len(getUses(c.mod.NamedFunction("runtime.startGoroutine"))) != 0 || len(getUses(c.mod.NamedFunction("runtime.yield"))) != 0 {
|
||||
// Program needs a scheduler. Start main.main as a goroutine and start
|
||||
// the scheduler.
|
||||
realMainWrapper := c.createGoroutineStartWrapper(realMain)
|
||||
@@ -150,10 +154,6 @@ func (c *Compiler) lowerTasks() error {
|
||||
zero := llvm.ConstInt(c.uintptrType, 0, false)
|
||||
c.createRuntimeCall("startGoroutine", []llvm.Value{realMainWrapper, zero}, "")
|
||||
c.createRuntimeCall("scheduler", nil, "")
|
||||
sleep := c.mod.NamedFunction("time.Sleep")
|
||||
if !sleep.IsNil() {
|
||||
sleep.ReplaceAllUsesWith(c.mod.NamedFunction("runtime.sleepCurrentTask"))
|
||||
}
|
||||
} else {
|
||||
// Program doesn't need a scheduler. Call main.main directly.
|
||||
c.builder.SetInsertPointBefore(mainCall)
|
||||
@@ -162,9 +162,6 @@ func (c *Compiler) lowerTasks() error {
|
||||
llvm.Undef(c.i8ptrType), // unused coroutine handle
|
||||
}
|
||||
c.createCall(realMain, params, "")
|
||||
// runtime.Goexit isn't needed so let it be optimized away by
|
||||
// globalopt.
|
||||
c.mod.NamedFunction("runtime.Goexit").SetLinkage(llvm.InternalLinkage)
|
||||
}
|
||||
mainCall.EraseFromParentAsInstruction()
|
||||
|
||||
@@ -195,7 +192,13 @@ func (c *Compiler) lowerCoroutines() error {
|
||||
// optionally followed by a call to runtime.scheduler().
|
||||
c.builder.SetInsertPointBefore(mainCall)
|
||||
realMain := c.mod.NamedFunction(c.ir.MainPkg().Pkg.Path() + ".main")
|
||||
c.builder.CreateCall(realMain, []llvm.Value{llvm.Undef(c.i8ptrType), llvm.ConstPointerNull(c.i8ptrType)}, "")
|
||||
var ph llvm.Value
|
||||
if needsScheduler {
|
||||
ph = c.createRuntimeCall("getFakeCoroutine", []llvm.Value{}, "")
|
||||
} else {
|
||||
ph = llvm.Undef(c.i8ptrType)
|
||||
}
|
||||
c.builder.CreateCall(realMain, []llvm.Value{llvm.Undef(c.i8ptrType), ph}, "")
|
||||
if needsScheduler {
|
||||
c.createRuntimeCall("scheduler", nil, "")
|
||||
}
|
||||
@@ -218,6 +221,12 @@ func (c *Compiler) lowerCoroutines() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func coroDebugPrintln(s ...interface{}) {
|
||||
if coroDebug {
|
||||
fmt.Println(s...)
|
||||
}
|
||||
}
|
||||
|
||||
// markAsyncFunctions does the bulk of the work of lowering goroutines. It
|
||||
// determines whether a scheduler is needed, and if it is, it transforms
|
||||
// blocking operations into goroutines and blocking calls into await calls.
|
||||
@@ -233,26 +242,14 @@ func (c *Compiler) lowerCoroutines() error {
|
||||
func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
|
||||
var worklist []llvm.Value
|
||||
|
||||
sleep := c.mod.NamedFunction("time.Sleep")
|
||||
if !sleep.IsNil() {
|
||||
worklist = append(worklist, sleep)
|
||||
}
|
||||
deadlock := c.mod.NamedFunction("runtime.deadlock")
|
||||
if !deadlock.IsNil() {
|
||||
worklist = append(worklist, deadlock)
|
||||
}
|
||||
chanSend := c.mod.NamedFunction("runtime.chanSend")
|
||||
if !chanSend.IsNil() {
|
||||
worklist = append(worklist, chanSend)
|
||||
}
|
||||
chanRecv := c.mod.NamedFunction("runtime.chanRecv")
|
||||
if !chanRecv.IsNil() {
|
||||
worklist = append(worklist, chanRecv)
|
||||
yield := c.mod.NamedFunction("runtime.yield")
|
||||
if !yield.IsNil() {
|
||||
worklist = append(worklist, yield)
|
||||
}
|
||||
|
||||
if len(worklist) == 0 {
|
||||
// There are no blocking operations, so no need to transform anything.
|
||||
return false, c.lowerMakeGoroutineCalls()
|
||||
return false, c.lowerMakeGoroutineCalls(false)
|
||||
}
|
||||
|
||||
// Find all async functions.
|
||||
@@ -269,6 +266,9 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
|
||||
if _, ok := asyncFuncs[f]; ok {
|
||||
continue // already processed
|
||||
}
|
||||
if f.Name() == "resume" {
|
||||
continue
|
||||
}
|
||||
// Add to set of async functions.
|
||||
asyncFuncs[f] = &asyncFunc{}
|
||||
asyncList = append(asyncList, f)
|
||||
@@ -312,11 +312,23 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
|
||||
|
||||
// Check whether a scheduler is needed.
|
||||
makeGoroutine := c.mod.NamedFunction("runtime.makeGoroutine")
|
||||
if c.GOOS == "js" && strings.HasPrefix(c.Triple, "wasm") {
|
||||
// JavaScript always needs a scheduler, as in general no blocking
|
||||
// operations are possible. Blocking operations block the browser UI,
|
||||
// which is very bad.
|
||||
needsScheduler = true
|
||||
if strings.HasPrefix(c.Triple, "avr") {
|
||||
needsScheduler = false
|
||||
getCoroutine := c.mod.NamedFunction("runtime.getCoroutine")
|
||||
for _, inst := range getUses(getCoroutine) {
|
||||
inst.ReplaceAllUsesWith(llvm.Undef(inst.Type()))
|
||||
inst.EraseFromParentAsInstruction()
|
||||
}
|
||||
yield := c.mod.NamedFunction("runtime.yield")
|
||||
for _, inst := range getUses(yield) {
|
||||
inst.EraseFromParentAsInstruction()
|
||||
}
|
||||
sleep := c.mod.NamedFunction("time.Sleep")
|
||||
for _, inst := range getUses(sleep) {
|
||||
c.builder.SetInsertPointBefore(inst)
|
||||
c.createRuntimeCall("avrSleep", []llvm.Value{inst.Operand(0)}, "")
|
||||
inst.EraseFromParentAsInstruction()
|
||||
}
|
||||
} else {
|
||||
// Only use a scheduler when an async goroutine is started. When the
|
||||
// goroutine is not async (does not do any blocking operation), no
|
||||
@@ -328,18 +340,353 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
|
||||
panic("expected const ptrtoint operand of runtime.makeGoroutine")
|
||||
}
|
||||
goroutine := ptrtoint.Operand(0)
|
||||
if goroutine.Name() == "runtime.fakeCoroutine" {
|
||||
continue
|
||||
}
|
||||
if _, ok := asyncFuncs[goroutine]; ok {
|
||||
needsScheduler = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if _, ok := asyncFuncs[c.mod.NamedFunction(c.ir.MainPkg().Pkg.Path()+".main")]; ok {
|
||||
needsScheduler = true
|
||||
}
|
||||
}
|
||||
|
||||
if !needsScheduler {
|
||||
// on wasm, we may still have calls to deadlock
|
||||
// replace these with an abort
|
||||
abort := c.mod.NamedFunction("runtime.abort")
|
||||
if deadlock := c.mod.NamedFunction("runtime.deadlock"); !deadlock.IsNil() {
|
||||
deadlock.ReplaceAllUsesWith(abort)
|
||||
}
|
||||
|
||||
// No scheduler is needed. Do not transform all functions here.
|
||||
// However, make sure that all go calls (which are all non-async) are
|
||||
// transformed into regular calls.
|
||||
return false, c.lowerMakeGoroutineCalls()
|
||||
return false, c.lowerMakeGoroutineCalls(false)
|
||||
}
|
||||
|
||||
if noret := c.mod.NamedFunction("runtime.noret"); noret.IsNil() {
|
||||
panic("missing noret")
|
||||
}
|
||||
|
||||
// replace indefinitely blocking yields
|
||||
getCoroutine := c.mod.NamedFunction("runtime.getCoroutine")
|
||||
coroDebugPrintln("replace indefinitely blocking yields")
|
||||
nonReturning := map[llvm.Value]bool{}
|
||||
for _, f := range asyncList {
|
||||
if f == yield {
|
||||
continue
|
||||
}
|
||||
coroDebugPrintln("scanning", f.Name())
|
||||
|
||||
var callsAsyncNotYield bool
|
||||
var callsYield bool
|
||||
var getsCoroutine bool
|
||||
for bb := f.EntryBasicBlock(); !bb.IsNil(); bb = llvm.NextBasicBlock(bb) {
|
||||
for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) {
|
||||
if !inst.IsACallInst().IsNil() {
|
||||
callee := inst.CalledValue()
|
||||
if callee == yield {
|
||||
callsYield = true
|
||||
} else if callee == getCoroutine {
|
||||
getsCoroutine = true
|
||||
} else if _, ok := asyncFuncs[callee]; ok {
|
||||
callsAsyncNotYield = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coroDebugPrintln("result", f.Name(), callsYield, getsCoroutine, callsAsyncNotYield)
|
||||
|
||||
if callsYield && !getsCoroutine && !callsAsyncNotYield {
|
||||
coroDebugPrintln("optimizing", f.Name())
|
||||
// calls yield without registering for a wakeup
|
||||
// this actually could otherwise wake up, but only in the case of really messed up undefined behavior
|
||||
// so everything after a yield is unreachable, so we can just inject a fake return
|
||||
delQueue := []llvm.Value{}
|
||||
for bb := f.EntryBasicBlock(); !bb.IsNil(); bb = llvm.NextBasicBlock(bb) {
|
||||
var broken bool
|
||||
|
||||
for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) {
|
||||
if !broken && !inst.IsACallInst().IsNil() && inst.CalledValue() == yield {
|
||||
coroDebugPrintln("broke", f.Name(), bb.AsValue().Name())
|
||||
broken = true
|
||||
c.builder.SetInsertPointBefore(inst)
|
||||
c.createRuntimeCall("noret", []llvm.Value{}, "")
|
||||
if f.Type().ElementType().ReturnType().TypeKind() == llvm.VoidTypeKind {
|
||||
c.builder.CreateRetVoid()
|
||||
} else {
|
||||
c.builder.CreateRet(llvm.Undef(f.Type().ElementType().ReturnType()))
|
||||
}
|
||||
}
|
||||
if broken {
|
||||
if inst.Type().TypeKind() != llvm.VoidTypeKind {
|
||||
inst.ReplaceAllUsesWith(llvm.Undef(inst.Type()))
|
||||
}
|
||||
delQueue = append(delQueue, inst)
|
||||
}
|
||||
}
|
||||
if !broken {
|
||||
coroDebugPrintln("did not break", f.Name(), bb.AsValue().Name())
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range delQueue {
|
||||
v.EraseFromParentAsInstruction()
|
||||
}
|
||||
|
||||
nonReturning[f] = true
|
||||
}
|
||||
}
|
||||
|
||||
// convert direct calls into an async call followed by a yield operation
|
||||
coroDebugPrintln("convert direct calls into an async call followed by a yield operation")
|
||||
for _, f := range asyncList {
|
||||
if f == yield {
|
||||
continue
|
||||
}
|
||||
coroDebugPrintln("scanning", f.Name())
|
||||
|
||||
var retAlloc llvm.Value
|
||||
|
||||
// Rewrite async calls
|
||||
for bb := f.EntryBasicBlock(); !bb.IsNil(); bb = llvm.NextBasicBlock(bb) {
|
||||
for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) {
|
||||
if !inst.IsACallInst().IsNil() {
|
||||
callee := inst.CalledValue()
|
||||
if _, ok := asyncFuncs[callee]; !ok || callee == yield {
|
||||
continue
|
||||
}
|
||||
|
||||
uses := getUses(inst)
|
||||
next := llvm.NextInstruction(inst)
|
||||
switch {
|
||||
case nonReturning[callee]:
|
||||
// callee blocks forever
|
||||
coroDebugPrintln("optimizing indefinitely blocking call", f.Name(), callee.Name())
|
||||
|
||||
// never calls getCoroutine - coroutine handle is irrelevant
|
||||
inst.SetOperand(inst.OperandsCount()-2, llvm.Undef(c.i8ptrType))
|
||||
|
||||
// insert return
|
||||
c.builder.SetInsertPointBefore(next)
|
||||
c.createRuntimeCall("noret", []llvm.Value{}, "")
|
||||
var retInst llvm.Value
|
||||
if f.Type().ElementType().ReturnType().TypeKind() == llvm.VoidTypeKind {
|
||||
retInst = c.builder.CreateRetVoid()
|
||||
} else {
|
||||
retInst = c.builder.CreateRet(llvm.Undef(f.Type().ElementType().ReturnType()))
|
||||
}
|
||||
|
||||
// delete everything after return
|
||||
for next := llvm.NextInstruction(retInst); !next.IsNil(); next = llvm.NextInstruction(retInst) {
|
||||
next.ReplaceAllUsesWith(llvm.Undef(retInst.Type()))
|
||||
next.EraseFromParentAsInstruction()
|
||||
}
|
||||
|
||||
continue
|
||||
case next.IsAReturnInst().IsNil():
|
||||
// not a return instruction
|
||||
coroDebugPrintln("not a return instruction", f.Name(), callee.Name())
|
||||
case callee.Type().ElementType().ReturnType() != f.Type().ElementType().ReturnType():
|
||||
// return types do not match
|
||||
coroDebugPrintln("return types do not match", f.Name(), callee.Name())
|
||||
case callee.Type().ElementType().ReturnType().TypeKind() == llvm.VoidTypeKind:
|
||||
fallthrough
|
||||
case next.Operand(0) == inst:
|
||||
// async tail call optimization - just pass parent handle
|
||||
coroDebugPrintln("doing async tail call opt", f.Name())
|
||||
|
||||
// insert before call
|
||||
c.builder.SetInsertPointBefore(inst)
|
||||
|
||||
// get parent handle
|
||||
parentHandle := c.createRuntimeCall("getParentHandle", []llvm.Value{}, "")
|
||||
|
||||
// pass parent handle directly into function
|
||||
inst.SetOperand(inst.OperandsCount()-2, parentHandle)
|
||||
|
||||
if inst.Type().TypeKind() != llvm.VoidTypeKind {
|
||||
// delete return value
|
||||
uses[0].SetOperand(0, llvm.Undef(inst.Type()))
|
||||
}
|
||||
|
||||
c.builder.SetInsertPointBefore(next)
|
||||
c.createRuntimeCall("yield", []llvm.Value{}, "")
|
||||
c.createRuntimeCall("noret", []llvm.Value{}, "")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
coroDebugPrintln("inserting regular call", f.Name(), callee.Name())
|
||||
c.builder.SetInsertPointBefore(inst)
|
||||
|
||||
// insert call to getCoroutine, this will be lowered later
|
||||
coro := c.createRuntimeCall("getCoroutine", []llvm.Value{}, "")
|
||||
|
||||
// provide coroutine handle to function
|
||||
inst.SetOperand(inst.OperandsCount()-2, coro)
|
||||
|
||||
// Allocate space for the return value.
|
||||
var retvalAlloca llvm.Value
|
||||
if inst.Type().TypeKind() != llvm.VoidTypeKind {
|
||||
if retAlloc.IsNil() {
|
||||
// insert at start of function
|
||||
c.builder.SetInsertPointBefore(f.EntryBasicBlock().FirstInstruction())
|
||||
|
||||
// allocate return value buffer
|
||||
retAlloc = c.builder.CreateAlloca(inst.Type(), "coro.retvalAlloca")
|
||||
}
|
||||
retvalAlloca = retAlloc
|
||||
|
||||
// call before function
|
||||
c.builder.SetInsertPointBefore(inst)
|
||||
|
||||
// cast buffer pointer to *i8
|
||||
data := c.builder.CreateBitCast(retvalAlloca, c.i8ptrType, "")
|
||||
|
||||
// set state pointer to return value buffer so it can be written back
|
||||
c.createRuntimeCall("setTaskStatePtr", []llvm.Value{coro, data}, "")
|
||||
}
|
||||
|
||||
// insert yield after starting function
|
||||
c.builder.SetInsertPointBefore(llvm.NextInstruction(inst))
|
||||
yieldCall := c.createRuntimeCall("yield", []llvm.Value{}, "")
|
||||
|
||||
if !retvalAlloca.IsNil() && !inst.FirstUse().IsNil() {
|
||||
// Load the return value from the alloca.
|
||||
// The callee has written the return value to it.
|
||||
c.builder.SetInsertPointBefore(llvm.NextInstruction(yieldCall))
|
||||
retval := c.builder.CreateLoad(retvalAlloca, "coro.retval")
|
||||
inst.ReplaceAllUsesWith(retval)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ditch unnecessary tail yields
|
||||
coroDebugPrintln("ditch unnecessary tail yields")
|
||||
noret := c.mod.NamedFunction("runtime.noret")
|
||||
for _, f := range asyncList {
|
||||
if f == yield {
|
||||
continue
|
||||
}
|
||||
coroDebugPrintln("scanning", f.Name())
|
||||
|
||||
// we can only ditch a yield if we can ditch all yields
|
||||
var yields []llvm.Value
|
||||
var canDitch bool
|
||||
scanYields:
|
||||
for bb := f.EntryBasicBlock(); !bb.IsNil(); bb = llvm.NextBasicBlock(bb) {
|
||||
for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) {
|
||||
if inst.IsACallInst().IsNil() || inst.CalledValue() != yield {
|
||||
continue
|
||||
}
|
||||
|
||||
yields = append(yields, inst)
|
||||
|
||||
// we can only ditch the yield if the next instruction is a void return *or* noret
|
||||
next := llvm.NextInstruction(inst)
|
||||
ditchable := false
|
||||
switch {
|
||||
case !next.IsACallInst().IsNil() && next.CalledValue() == noret:
|
||||
coroDebugPrintln("ditching yield with noret", f.Name())
|
||||
ditchable = true
|
||||
case !next.IsAReturnInst().IsNil() && f.Type().ElementType().ReturnType().TypeKind() == llvm.VoidTypeKind:
|
||||
coroDebugPrintln("ditching yield with void return", f.Name())
|
||||
ditchable = true
|
||||
case !next.IsAReturnInst().IsNil():
|
||||
coroDebugPrintln("not ditching because return is not void", f.Name(), f.Type().ElementType().ReturnType().String())
|
||||
default:
|
||||
coroDebugPrintln("not ditching", f.Name())
|
||||
}
|
||||
if !ditchable {
|
||||
// unditchable yield
|
||||
canDitch = false
|
||||
break scanYields
|
||||
}
|
||||
|
||||
// ditchable yield
|
||||
canDitch = true
|
||||
}
|
||||
}
|
||||
|
||||
if canDitch {
|
||||
coroDebugPrintln("ditching all in", f.Name())
|
||||
for _, inst := range yields {
|
||||
if !llvm.NextInstruction(inst).IsAReturnInst().IsNil() {
|
||||
// insert noret
|
||||
coroDebugPrintln("insering noret", f.Name())
|
||||
c.builder.SetInsertPointBefore(inst)
|
||||
c.createRuntimeCall("noret", []llvm.Value{}, "")
|
||||
}
|
||||
|
||||
// delete original yield
|
||||
inst.EraseFromParentAsInstruction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate return reactivations
|
||||
coroDebugPrintln("generate return reactivations")
|
||||
for _, f := range asyncList {
|
||||
if f == yield {
|
||||
continue
|
||||
}
|
||||
coroDebugPrintln("scanning", f.Name())
|
||||
|
||||
var retPtr llvm.Value
|
||||
for bb := f.EntryBasicBlock(); !bb.IsNil(); bb = llvm.NextBasicBlock(bb) {
|
||||
block:
|
||||
for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) {
|
||||
switch {
|
||||
case !inst.IsACallInst().IsNil() && inst.CalledValue() == noret:
|
||||
// does not return normally - skip this basic block
|
||||
coroDebugPrintln("noret found - skipping", f.Name(), bb.AsValue().Name())
|
||||
break block
|
||||
case !inst.IsAReturnInst().IsNil():
|
||||
// return instruction - rewrite to reactivation
|
||||
coroDebugPrintln("adding return reactivation", f.Name(), bb.AsValue().Name())
|
||||
if f.Type().ElementType().ReturnType().TypeKind() != llvm.VoidTypeKind {
|
||||
// returns something
|
||||
if retPtr.IsNil() {
|
||||
coroDebugPrintln("adding return pointer get", f.Name())
|
||||
|
||||
// get return pointer in entry block
|
||||
c.builder.SetInsertPointBefore(f.EntryBasicBlock().FirstInstruction())
|
||||
parentHandle := c.createRuntimeCall("getParentHandle", []llvm.Value{}, "")
|
||||
ptr := c.createRuntimeCall("getTaskStatePtr", []llvm.Value{parentHandle}, "")
|
||||
retPtr = c.builder.CreateBitCast(ptr, llvm.PointerType(f.Type().ElementType().ReturnType(), 0), "retPtr")
|
||||
}
|
||||
|
||||
coroDebugPrintln("adding return store", f.Name(), bb.AsValue().Name())
|
||||
|
||||
// store result into return pointer
|
||||
c.builder.SetInsertPointBefore(inst)
|
||||
c.builder.CreateStore(inst.Operand(0), retPtr)
|
||||
|
||||
// delete return value
|
||||
inst.SetOperand(0, llvm.Undef(inst.Type()))
|
||||
}
|
||||
|
||||
// insert reactivation call
|
||||
c.builder.SetInsertPointBefore(inst)
|
||||
parentHandle := c.createRuntimeCall("getParentHandle", []llvm.Value{}, "")
|
||||
c.createRuntimeCall("activateTask", []llvm.Value{parentHandle}, "")
|
||||
|
||||
// mark as noret
|
||||
c.builder.SetInsertPointBefore(inst)
|
||||
c.createRuntimeCall("noret", []llvm.Value{}, "")
|
||||
break block
|
||||
|
||||
// DO NOT ERASE THE RETURN!!!!!!!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a few LLVM intrinsics for coroutine support.
|
||||
@@ -362,45 +709,62 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
|
||||
coroFreeType := llvm.FunctionType(c.i8ptrType, []llvm.Type{c.ctx.TokenType(), c.i8ptrType}, false)
|
||||
coroFreeFunc := llvm.AddFunction(c.mod, "llvm.coro.free", coroFreeType)
|
||||
|
||||
// Transform all async functions into coroutines.
|
||||
// split blocks and add LLVM coroutine intrinsics
|
||||
coroDebugPrintln("split blocks and add LLVM coroutine intrinsics")
|
||||
for _, f := range asyncList {
|
||||
if f == sleep || f == deadlock || f == chanSend || f == chanRecv {
|
||||
if f == yield {
|
||||
continue
|
||||
}
|
||||
|
||||
frame := asyncFuncs[f]
|
||||
frame.cleanupBlock = c.ctx.AddBasicBlock(f, "task.cleanup")
|
||||
frame.suspendBlock = c.ctx.AddBasicBlock(f, "task.suspend")
|
||||
frame.unreachableBlock = c.ctx.AddBasicBlock(f, "task.unreachable")
|
||||
|
||||
// Scan for async calls and return instructions that need to have
|
||||
// suspend points inserted.
|
||||
var asyncCalls []llvm.Value
|
||||
var returns []llvm.Value
|
||||
// find calls to yield
|
||||
var yieldCalls []llvm.Value
|
||||
for bb := f.EntryBasicBlock(); !bb.IsNil(); bb = llvm.NextBasicBlock(bb) {
|
||||
for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) {
|
||||
if !inst.IsACallInst().IsNil() {
|
||||
callee := inst.CalledValue()
|
||||
if _, ok := asyncFuncs[callee]; !ok || callee == sleep || callee == deadlock || callee == chanSend || callee == chanRecv {
|
||||
continue
|
||||
}
|
||||
asyncCalls = append(asyncCalls, inst)
|
||||
} else if !inst.IsAReturnInst().IsNil() {
|
||||
returns = append(returns, inst)
|
||||
if !inst.IsACallInst().IsNil() && inst.CalledValue() == yield {
|
||||
yieldCalls = append(yieldCalls, inst)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Coroutine setup.
|
||||
if len(yieldCalls) == 0 {
|
||||
// no yields - we do not have to LLVM-ify this
|
||||
coroDebugPrintln("skipping", f.Name())
|
||||
for bb := f.EntryBasicBlock(); !bb.IsNil(); bb = llvm.NextBasicBlock(bb) {
|
||||
for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) {
|
||||
if !inst.IsACallInst().IsNil() && inst.CalledValue() == getCoroutine {
|
||||
// no seperate local task - replace getCoroutine with getParentHandle
|
||||
c.builder.SetInsertPointBefore(inst)
|
||||
inst.ReplaceAllUsesWith(c.createRuntimeCall("getParentHandle", []llvm.Value{}, ""))
|
||||
inst.EraseFromParentAsInstruction()
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
coroDebugPrintln("converting", f.Name())
|
||||
|
||||
// get frame data to mess with
|
||||
frame := asyncFuncs[f]
|
||||
|
||||
// add basic blocks to put cleanup and suspend code
|
||||
frame.cleanupBlock = c.ctx.AddBasicBlock(f, "task.cleanup")
|
||||
frame.suspendBlock = c.ctx.AddBasicBlock(f, "task.suspend")
|
||||
|
||||
// at start of function
|
||||
c.builder.SetInsertPointBefore(f.EntryBasicBlock().FirstInstruction())
|
||||
taskState := c.builder.CreateAlloca(c.getLLVMRuntimeType("taskState"), "task.state")
|
||||
stateI8 := c.builder.CreateBitCast(taskState, c.i8ptrType, "task.state.i8")
|
||||
|
||||
// get LLVM-assigned coroutine ID
|
||||
id := c.builder.CreateCall(coroIdFunc, []llvm.Value{
|
||||
llvm.ConstInt(c.ctx.Int32Type(), 0, false),
|
||||
stateI8,
|
||||
llvm.ConstNull(c.i8ptrType),
|
||||
llvm.ConstNull(c.i8ptrType),
|
||||
}, "task.token")
|
||||
|
||||
// allocate buffer for task struct
|
||||
size := c.builder.CreateCall(coroSizeFunc, nil, "task.size")
|
||||
if c.targetData.TypeAllocSize(size.Type()) > c.targetData.TypeAllocSize(c.uintptrType) {
|
||||
size = c.builder.CreateTrunc(size, c.uintptrType, "task.size.uintptr")
|
||||
@@ -411,108 +775,10 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
|
||||
if c.needsStackObjects() {
|
||||
c.trackPointer(data)
|
||||
}
|
||||
|
||||
// invoke llvm.coro.begin intrinsic and save task pointer
|
||||
frame.taskHandle = c.builder.CreateCall(coroBeginFunc, []llvm.Value{id, data}, "task.handle")
|
||||
|
||||
// Modify async calls so this function suspends right after the child
|
||||
// returns, because the child is probably not finished yet. Wait until
|
||||
// the child reactivates the parent.
|
||||
for _, inst := range asyncCalls {
|
||||
inst.SetOperand(inst.OperandsCount()-2, frame.taskHandle)
|
||||
|
||||
// Split this basic block.
|
||||
await := c.splitBasicBlock(inst, llvm.NextBasicBlock(c.builder.GetInsertBlock()), "task.await")
|
||||
|
||||
// Allocate space for the return value.
|
||||
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("setTaskStatePtr", []llvm.Value{frame.taskHandle, data}, "")
|
||||
}
|
||||
|
||||
// Suspend.
|
||||
c.builder.SetInsertPointAtEnd(inst.InstructionParent())
|
||||
continuePoint := c.builder.CreateCall(coroSuspendFunc, []llvm.Value{
|
||||
llvm.ConstNull(c.ctx.TokenType()),
|
||||
llvm.ConstInt(c.ctx.Int1Type(), 0, false),
|
||||
}, "")
|
||||
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(), 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
|
||||
// reactivate the parent coroutine.
|
||||
for _, inst := range returns {
|
||||
// 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)
|
||||
f.RemoveEnumAttributeAtIndex(f.ParamsCount(), kindID)
|
||||
}
|
||||
|
||||
c.builder.SetInsertPointBefore(inst)
|
||||
|
||||
var parentHandle llvm.Value
|
||||
if f.Linkage() == llvm.ExternalLinkage {
|
||||
// Exported function.
|
||||
// Note that getTaskStatePtr will panic if it is called with
|
||||
// a nil pointer, so blocking exported functions that try to
|
||||
// return anything will not work.
|
||||
parentHandle = llvm.ConstPointerNull(c.i8ptrType)
|
||||
} else {
|
||||
parentHandle = f.LastParam()
|
||||
if parentHandle.IsNil() || parentHandle.Name() != "parentHandle" {
|
||||
// sanity check
|
||||
panic("trying to make exported function async: " + f.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// 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("getTaskStatePtr", []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}, "")
|
||||
|
||||
// Suspend this coroutine.
|
||||
// It would look like this is unnecessary, but if this
|
||||
// suspend point is left out, it leads to undefined
|
||||
// behavior somehow (with the unreachable instruction).
|
||||
continuePoint := c.builder.CreateCall(coroSuspendFunc, []llvm.Value{
|
||||
llvm.ConstNull(c.ctx.TokenType()),
|
||||
llvm.ConstInt(c.ctx.Int1Type(), 0, false),
|
||||
}, "ret")
|
||||
sw := c.builder.CreateSwitch(continuePoint, frame.suspendBlock, 2)
|
||||
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), frame.unreachableBlock)
|
||||
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), frame.cleanupBlock)
|
||||
inst.EraseFromParentAsInstruction()
|
||||
}
|
||||
|
||||
// Coroutine cleanup. Free resources associated with this coroutine.
|
||||
c.builder.SetInsertPointAtEnd(frame.cleanupBlock)
|
||||
mem := c.builder.CreateCall(coroFreeFunc, []llvm.Value{id, frame.taskHandle}, "task.data.free")
|
||||
@@ -529,106 +795,96 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
|
||||
c.builder.CreateRet(llvm.Undef(returnType))
|
||||
}
|
||||
|
||||
// Coroutine exit. All final suspends (return instructions) will branch
|
||||
// here.
|
||||
c.builder.SetInsertPointAtEnd(frame.unreachableBlock)
|
||||
c.builder.CreateUnreachable()
|
||||
for _, inst := range yieldCalls {
|
||||
// Replace call to yield with a suspension of the coroutine.
|
||||
c.builder.SetInsertPointBefore(inst)
|
||||
continuePoint := c.builder.CreateCall(coroSuspendFunc, []llvm.Value{
|
||||
llvm.ConstNull(c.ctx.TokenType()),
|
||||
llvm.ConstInt(c.ctx.Int1Type(), 0, false),
|
||||
}, "")
|
||||
wakeup := c.splitBasicBlock(inst, llvm.NextBasicBlock(c.builder.GetInsertBlock()), "task.wakeup")
|
||||
c.builder.SetInsertPointBefore(inst)
|
||||
sw := c.builder.CreateSwitch(continuePoint, frame.suspendBlock, 2)
|
||||
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), wakeup)
|
||||
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), frame.cleanupBlock)
|
||||
inst.EraseFromParentAsInstruction()
|
||||
}
|
||||
ditchQueue := []llvm.Value{}
|
||||
for bb := f.EntryBasicBlock(); !bb.IsNil(); bb = llvm.NextBasicBlock(bb) {
|
||||
for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) {
|
||||
if !inst.IsACallInst().IsNil() && inst.CalledValue() == getCoroutine {
|
||||
// replace getCoroutine calls with the task handle
|
||||
inst.ReplaceAllUsesWith(frame.taskHandle)
|
||||
ditchQueue = append(ditchQueue, inst)
|
||||
}
|
||||
if !inst.IsACallInst().IsNil() && inst.CalledValue() == noret {
|
||||
// replace tail yield with jump to cleanup, otherwise we end up with undefined behavior
|
||||
c.builder.SetInsertPointBefore(inst)
|
||||
c.builder.CreateBr(frame.cleanupBlock)
|
||||
ditchQueue = append(ditchQueue, inst, llvm.NextInstruction(inst))
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, v := range ditchQueue {
|
||||
v.EraseFromParentAsInstruction()
|
||||
}
|
||||
}
|
||||
|
||||
// Replace calls to runtime.getCoroutineCall with the coroutine of this
|
||||
// frame.
|
||||
for _, getCoroutineCall := range getUses(c.mod.NamedFunction("runtime.getCoroutine")) {
|
||||
frame := asyncFuncs[getCoroutineCall.InstructionParent().Parent()]
|
||||
getCoroutineCall.ReplaceAllUsesWith(frame.taskHandle)
|
||||
getCoroutineCall.EraseFromParentAsInstruction()
|
||||
// check for leftover calls to getCoroutine
|
||||
if uses := getUses(getCoroutine); len(uses) > 0 {
|
||||
useNames := make([]string, 0, len(uses))
|
||||
for _, u := range uses {
|
||||
if u.InstructionParent().Parent().Name() == "runtime.llvmCoroRefHolder" {
|
||||
continue
|
||||
}
|
||||
useNames = append(useNames, u.InstructionParent().Parent().Name())
|
||||
}
|
||||
if len(useNames) > 0 {
|
||||
panic("bad use of getCoroutine: " + strings.Join(useNames, ","))
|
||||
}
|
||||
}
|
||||
|
||||
// Transform calls to time.Sleep() into coroutine suspend points.
|
||||
for _, sleepCall := range getUses(sleep) {
|
||||
// sleepCall must be a call instruction.
|
||||
frame := asyncFuncs[sleepCall.InstructionParent().Parent()]
|
||||
duration := sleepCall.Operand(0)
|
||||
|
||||
// Set task state to TASK_STATE_SLEEP and set the duration.
|
||||
c.builder.SetInsertPointBefore(sleepCall)
|
||||
c.createRuntimeCall("sleepTask", []llvm.Value{frame.taskHandle, duration}, "")
|
||||
|
||||
// Yield to scheduler.
|
||||
continuePoint := c.builder.CreateCall(coroSuspendFunc, []llvm.Value{
|
||||
llvm.ConstNull(c.ctx.TokenType()),
|
||||
llvm.ConstInt(c.ctx.Int1Type(), 0, false),
|
||||
}, "")
|
||||
wakeup := c.splitBasicBlock(sleepCall, llvm.NextBasicBlock(c.builder.GetInsertBlock()), "task.wakeup")
|
||||
c.builder.SetInsertPointBefore(sleepCall)
|
||||
sw := c.builder.CreateSwitch(continuePoint, frame.suspendBlock, 2)
|
||||
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), wakeup)
|
||||
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), frame.cleanupBlock)
|
||||
sleepCall.EraseFromParentAsInstruction()
|
||||
// rewrite calls to getParentHandle
|
||||
for _, inst := range getUses(c.mod.NamedFunction("runtime.getParentHandle")) {
|
||||
f := inst.InstructionParent().Parent()
|
||||
var parentHandle llvm.Value
|
||||
parentHandle = f.LastParam()
|
||||
if parentHandle.IsNil() || parentHandle.Name() != "parentHandle" {
|
||||
// sanity check
|
||||
panic("trying to make exported function async: " + f.Name())
|
||||
}
|
||||
inst.ReplaceAllUsesWith(parentHandle)
|
||||
inst.EraseFromParentAsInstruction()
|
||||
}
|
||||
|
||||
// Transform calls to runtime.deadlock into coroutine suspends (without
|
||||
// resume).
|
||||
for _, deadlockCall := range getUses(deadlock) {
|
||||
// deadlockCall must be a call instruction.
|
||||
frame := asyncFuncs[deadlockCall.InstructionParent().Parent()]
|
||||
|
||||
// Exit coroutine.
|
||||
c.builder.SetInsertPointBefore(deadlockCall)
|
||||
continuePoint := c.builder.CreateCall(coroSuspendFunc, []llvm.Value{
|
||||
llvm.ConstNull(c.ctx.TokenType()),
|
||||
llvm.ConstInt(c.ctx.Int1Type(), 0, false),
|
||||
}, "")
|
||||
c.splitBasicBlock(deadlockCall, llvm.NextBasicBlock(c.builder.GetInsertBlock()), "task.wakeup.dead")
|
||||
c.builder.SetInsertPointBefore(deadlockCall)
|
||||
sw := c.builder.CreateSwitch(continuePoint, frame.suspendBlock, 2)
|
||||
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), frame.unreachableBlock)
|
||||
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), frame.cleanupBlock)
|
||||
deadlockCall.EraseFromParentAsInstruction()
|
||||
// ditch invalid function attributes
|
||||
bads := []llvm.Value{c.mod.NamedFunction("runtime.setTaskStatePtr")}
|
||||
for _, f := range append(bads, asyncList...) {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transform calls to runtime.chanSend into channel send operations.
|
||||
for _, sendOp := range getUses(chanSend) {
|
||||
// sendOp must be a call instruction.
|
||||
frame := asyncFuncs[sendOp.InstructionParent().Parent()]
|
||||
|
||||
// Yield to scheduler.
|
||||
c.builder.SetInsertPointBefore(llvm.NextInstruction(sendOp))
|
||||
continuePoint := c.builder.CreateCall(coroSuspendFunc, []llvm.Value{
|
||||
llvm.ConstNull(c.ctx.TokenType()),
|
||||
llvm.ConstInt(c.ctx.Int1Type(), 0, false),
|
||||
}, "")
|
||||
sw := c.builder.CreateSwitch(continuePoint, frame.suspendBlock, 2)
|
||||
wakeup := c.splitBasicBlock(sw, llvm.NextBasicBlock(c.builder.GetInsertBlock()), "task.sent")
|
||||
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), wakeup)
|
||||
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), frame.cleanupBlock)
|
||||
// eliminate noret
|
||||
for _, inst := range getUses(noret) {
|
||||
inst.EraseFromParentAsInstruction()
|
||||
}
|
||||
|
||||
// Transform calls to runtime.chanRecv into channel receive operations.
|
||||
for _, recvOp := range getUses(chanRecv) {
|
||||
// recvOp must be a call instruction.
|
||||
frame := asyncFuncs[recvOp.InstructionParent().Parent()]
|
||||
|
||||
// Yield to scheduler.
|
||||
c.builder.SetInsertPointBefore(llvm.NextInstruction(recvOp))
|
||||
continuePoint := c.builder.CreateCall(coroSuspendFunc, []llvm.Value{
|
||||
llvm.ConstNull(c.ctx.TokenType()),
|
||||
llvm.ConstInt(c.ctx.Int1Type(), 0, false),
|
||||
}, "")
|
||||
sw := c.builder.CreateSwitch(continuePoint, frame.suspendBlock, 2)
|
||||
wakeup := c.splitBasicBlock(sw, llvm.NextBasicBlock(c.builder.GetInsertBlock()), "task.received")
|
||||
c.builder.SetInsertPointAtEnd(recvOp.InstructionParent())
|
||||
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), wakeup)
|
||||
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), frame.cleanupBlock)
|
||||
}
|
||||
|
||||
return true, c.lowerMakeGoroutineCalls()
|
||||
return true, c.lowerMakeGoroutineCalls(true)
|
||||
}
|
||||
|
||||
// Lower runtime.makeGoroutine calls to regular call instructions. This is done
|
||||
// after the regular goroutine transformations. The started goroutines are
|
||||
// either non-blocking (in which case they can be called directly) or blocking,
|
||||
// in which case they will ask the scheduler themselves to be rescheduled.
|
||||
func (c *Compiler) lowerMakeGoroutineCalls() error {
|
||||
func (c *Compiler) lowerMakeGoroutineCalls(sched bool) error {
|
||||
// The following Go code:
|
||||
// go startedGoroutine()
|
||||
//
|
||||
@@ -661,13 +917,21 @@ func (c *Compiler) lowerMakeGoroutineCalls() error {
|
||||
for i := 0; i < realCall.OperandsCount()-1; i++ {
|
||||
params = append(params, realCall.Operand(i))
|
||||
}
|
||||
params[len(params)-1] = llvm.ConstPointerNull(c.i8ptrType) // parent coroutine handle (must be nil)
|
||||
c.builder.SetInsertPointBefore(realCall)
|
||||
if (!sched) || goroutine.InstructionParent().Parent() == c.mod.NamedFunction("runtime.getFakeCoroutine") {
|
||||
params[len(params)-1] = llvm.Undef(c.i8ptrType)
|
||||
} else {
|
||||
params[len(params)-1] = c.createRuntimeCall("getFakeCoroutine", []llvm.Value{}, "") // parent coroutine handle (must not be nil)
|
||||
}
|
||||
c.builder.CreateCall(origFunc, params, "")
|
||||
realCall.EraseFromParentAsInstruction()
|
||||
inttoptrOut.EraseFromParentAsInstruction()
|
||||
goroutine.EraseFromParentAsInstruction()
|
||||
}
|
||||
|
||||
if !sched && len(getUses(c.mod.NamedFunction("runtime.getFakeCoroutine"))) > 0 {
|
||||
panic("getFakeCoroutine used without scheduler")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ func (c *Compiler) Optimize(optLevel, sizeLevel int, inlinerThreshold uint) erro
|
||||
}
|
||||
|
||||
// After TinyGo-specific transforms have finished, undo exporting these functions.
|
||||
for _, name := range functionsUsedInTransforms {
|
||||
for _, name := range c.getFunctionsUsedInTransforms() {
|
||||
fn := c.mod.NamedFunction(name)
|
||||
if fn.IsNil() {
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user