all: rewrite goroutine lowering

Before this commit, goroutine support was spread through the compiler.
This commit changes this support, so that the compiler itself only
generates simple intrinsics and leaves the real support to a compiler
pass that runs as one of the TinyGo-specific optimization passes.

The biggest change, that was done together with the rewrite, was support
for goroutines in WebAssembly for JavaScript. The challenge in
JavaScript is that in general no blocking operations are allowed, which
means that programs that call time.Sleep() but do not start goroutines
also have to be scheduled by the scheduler.
Этот коммит содержится в:
Ayke van Laethem 2019-01-10 16:54:09 +01:00
родитель 072ef603fe
коммит 602c264749
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: E97FF5335DFDFDED
24 изменённых файлов: 807 добавлений и 483 удалений

18
Gopkg.lock сгенерированный
Просмотреть файл

@ -3,30 +3,20 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:f250e2a6d7e4f9ebc5ba37e5e2ec91b46eb1399ee43f2fdaeb20cd4fd1aeee59" digest = "1:747e7010f73dc26e5cb298444f6f1b4442ae155ab87e44080686b94a5f9d6c90"
name = "github.com/aykevl/go-llvm" name = "github.com/aykevl/go-llvm"
packages = ["."] packages = ["."]
pruneopts = "UT" pruneopts = "UT"
revision = "d8539684f173a591ea9474d6262ac47ef2277d64" revision = "8a0a627130a54562f55b1c2a9d7b8019ff14d9f1"
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:d1102ae84d8c9318db4ce2ad2673eb2bf54569ab2a4a5d57e70d8aef726b681d" digest = "1:84316faef4ea12d34dde3b3e6dab682715a23b1c2bb8ab82cec9ab619766e214"
name = "golang.org/x/tools" name = "golang.org/x/tools"
packages = [ packages = [
"go/ast/astutil", "go/ast/astutil",
"go/buildutil",
"go/gcexportdata",
"go/internal/cgo",
"go/internal/gcimporter",
"go/loader",
"go/packages",
"go/ssa", "go/ssa",
"go/ssa/ssautil",
"go/types/typeutil", "go/types/typeutil",
"internal/fastwalk",
"internal/gopathwalk",
"internal/semver",
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "3e7aa9e59977626dc60433e9aeadf1bb63d28295" revision = "3e7aa9e59977626dc60433e9aeadf1bb63d28295"
@ -36,9 +26,7 @@
analyzer-version = 1 analyzer-version = 1
input-imports = [ input-imports = [
"github.com/aykevl/go-llvm", "github.com/aykevl/go-llvm",
"golang.org/x/tools/go/loader",
"golang.org/x/tools/go/ssa", "golang.org/x/tools/go/ssa",
"golang.org/x/tools/go/ssa/ssautil",
] ]
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

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

@ -22,6 +22,7 @@ func (c *Compiler) createRuntimeCall(fnName string, args []llvm.Value, name stri
fn := c.ir.GetFunction(member.(*ssa.Function)) fn := c.ir.GetFunction(member.(*ssa.Function))
if !fn.IsExported() { if !fn.IsExported() {
args = append(args, llvm.Undef(c.i8ptrType)) // unused context parameter args = append(args, llvm.Undef(c.i8ptrType)) // unused context parameter
args = append(args, llvm.ConstPointerNull(c.i8ptrType)) // coroutine handle
} }
return c.createCall(fn.LLVMFn, args, name) return c.createCall(fn.LLVMFn, args, name)
} }

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

@ -59,12 +59,6 @@ type Compiler struct {
intType llvm.Type intType llvm.Type
i8ptrType llvm.Type // for convenience i8ptrType llvm.Type // for convenience
uintptrType llvm.Type uintptrType llvm.Type
coroIdFunc llvm.Value
coroSizeFunc llvm.Value
coroBeginFunc llvm.Value
coroSuspendFunc llvm.Value
coroEndFunc llvm.Value
coroFreeFunc llvm.Value
initFuncs []llvm.Value initFuncs []llvm.Value
interfaceInvokeWrappers []interfaceInvokeWrapper interfaceInvokeWrappers []interfaceInvokeWrapper
ir *ir.Program ir *ir.Program
@ -77,10 +71,7 @@ type Frame struct {
blockExits map[*ssa.BasicBlock]llvm.BasicBlock // these are the exit blocks blockExits map[*ssa.BasicBlock]llvm.BasicBlock // these are the exit blocks
currentBlock *ssa.BasicBlock currentBlock *ssa.BasicBlock
phis []Phi phis []Phi
blocking bool
taskHandle llvm.Value taskHandle llvm.Value
cleanupBlock llvm.BasicBlock
suspendBlock llvm.BasicBlock
deferPtr llvm.Value deferPtr llvm.Value
difunc llvm.Metadata difunc llvm.Metadata
allDeferFuncs []interface{} allDeferFuncs []interface{}
@ -133,24 +124,6 @@ func NewCompiler(pkgName string, config Config) (*Compiler, error) {
} }
c.i8ptrType = llvm.PointerType(c.ctx.Int8Type(), 0) c.i8ptrType = llvm.PointerType(c.ctx.Int8Type(), 0)
coroIdType := llvm.FunctionType(c.ctx.TokenType(), []llvm.Type{c.ctx.Int32Type(), c.i8ptrType, c.i8ptrType, c.i8ptrType}, false)
c.coroIdFunc = llvm.AddFunction(c.mod, "llvm.coro.id", coroIdType)
coroSizeType := llvm.FunctionType(c.ctx.Int32Type(), nil, false)
c.coroSizeFunc = llvm.AddFunction(c.mod, "llvm.coro.size.i32", coroSizeType)
coroBeginType := llvm.FunctionType(c.i8ptrType, []llvm.Type{c.ctx.TokenType(), c.i8ptrType}, false)
c.coroBeginFunc = llvm.AddFunction(c.mod, "llvm.coro.begin", coroBeginType)
coroSuspendType := llvm.FunctionType(c.ctx.Int8Type(), []llvm.Type{c.ctx.TokenType(), c.ctx.Int1Type()}, false)
c.coroSuspendFunc = llvm.AddFunction(c.mod, "llvm.coro.suspend", coroSuspendType)
coroEndType := llvm.FunctionType(c.ctx.Int1Type(), []llvm.Type{c.i8ptrType, c.ctx.Int1Type()}, false)
c.coroEndFunc = llvm.AddFunction(c.mod, "llvm.coro.end", coroEndType)
coroFreeType := llvm.FunctionType(c.i8ptrType, []llvm.Type{c.ctx.TokenType(), c.i8ptrType}, false)
c.coroFreeFunc = llvm.AddFunction(c.mod, "llvm.coro.free", coroFreeType)
return c, nil return c, nil
} }
@ -237,12 +210,8 @@ func (c *Compiler) Compile(mainPath string) error {
c.ir = ir.NewProgram(lprogram, mainPath) c.ir = ir.NewProgram(lprogram, mainPath)
// Run some DCE and analysis passes. The results are later used by the // Run a simple dead code elimination pass.
// compiler. c.ir.SimpleDCE()
c.ir.SimpleDCE() // remove most dead code
c.ir.AnalyseCallgraph() // set up callgraph
c.ir.AnalyseBlockingRecursive() // make all parents of blocking calls blocking (transitively)
c.ir.AnalyseGoCalls() // check whether we need a scheduler
// Initialize debug information. // Initialize debug information.
c.cu = c.dibuilder.CreateCompileUnit(llvm.DICompileUnit{ c.cu = c.dibuilder.CreateCompileUnit(llvm.DICompileUnit{
@ -387,33 +356,19 @@ func (c *Compiler) Compile(mainPath string) error {
block := c.ctx.AddBasicBlock(initFn.LLVMFn, "entry") block := c.ctx.AddBasicBlock(initFn.LLVMFn, "entry")
c.builder.SetInsertPointAtEnd(block) c.builder.SetInsertPointAtEnd(block)
for _, fn := range c.initFuncs { for _, fn := range c.initFuncs {
c.builder.CreateCall(fn, []llvm.Value{llvm.Undef(c.i8ptrType)}, "") c.builder.CreateCall(fn, []llvm.Value{llvm.Undef(c.i8ptrType), llvm.Undef(c.i8ptrType)}, "")
} }
c.builder.CreateRetVoid() c.builder.CreateRetVoid()
// Add a wrapper for the main.main function, either calling it directly or // Conserve for goroutine lowering. Without marking these as external, they
// setting up the scheduler with it. // would be optimized away.
mainWrapper := c.ir.GetFunction(c.ir.Program.ImportedPackage("runtime").Members["mainWrapper"].(*ssa.Function))
mainWrapper.LLVMFn.SetLinkage(llvm.InternalLinkage)
mainWrapper.LLVMFn.SetUnnamedAddr(true)
if c.Debug {
difunc, err := c.attachDebugInfo(mainWrapper)
if err != nil {
return err
}
pos := c.ir.Program.Fset.Position(mainWrapper.Pos())
c.builder.SetCurrentDebugLocation(uint(pos.Line), uint(pos.Column), difunc, llvm.Metadata{})
}
block = c.ctx.AddBasicBlock(mainWrapper.LLVMFn, "entry")
c.builder.SetInsertPointAtEnd(block)
realMain := c.mod.NamedFunction(c.ir.MainPkg().Pkg.Path() + ".main") realMain := c.mod.NamedFunction(c.ir.MainPkg().Pkg.Path() + ".main")
if c.ir.NeedsScheduler() { realMain.SetLinkage(llvm.ExternalLinkage) // keep alive until goroutine lowering
coroutine := c.builder.CreateCall(realMain, []llvm.Value{llvm.ConstPointerNull(c.i8ptrType), llvm.Undef(c.i8ptrType)}, "") c.mod.NamedFunction("runtime.alloc").SetLinkage(llvm.ExternalLinkage)
c.createRuntimeCall("scheduler", []llvm.Value{coroutine}, "") c.mod.NamedFunction("runtime.free").SetLinkage(llvm.ExternalLinkage)
} else { c.mod.NamedFunction("runtime.sleepTask").SetLinkage(llvm.ExternalLinkage)
c.builder.CreateCall(realMain, []llvm.Value{llvm.Undef(c.i8ptrType)}, "") c.mod.NamedFunction("runtime.activateTask").SetLinkage(llvm.ExternalLinkage)
} c.mod.NamedFunction("runtime.scheduler").SetLinkage(llvm.ExternalLinkage)
c.builder.CreateRetVoid()
// see: https://reviews.llvm.org/D18355 // see: https://reviews.llvm.org/D18355
c.mod.AddNamedMetadataOperand("llvm.module.flags", c.mod.AddNamedMetadataOperand("llvm.module.flags",
@ -535,7 +490,8 @@ func (c *Compiler) getLLVMType(goType types.Type) (llvm.Type, error) {
} }
// make a closure type (with a function pointer type inside): // make a closure type (with a function pointer type inside):
// {context, funcptr} // {context, funcptr}
paramTypes = append(paramTypes, c.i8ptrType) paramTypes = append(paramTypes, c.i8ptrType) // context
paramTypes = append(paramTypes, c.i8ptrType) // parent coroutine
ptr := llvm.PointerType(llvm.FunctionType(returnType, paramTypes, false), 0) ptr := llvm.PointerType(llvm.FunctionType(returnType, paramTypes, false), 0)
ptr = c.ctx.StructType([]llvm.Type{c.i8ptrType, ptr}, false) ptr = c.ctx.StructType([]llvm.Type{c.i8ptrType, ptr}, false)
return ptr, nil return ptr, nil
@ -676,16 +632,10 @@ func (c *Compiler) parseFuncDecl(f *ir.Function) (*Frame, error) {
locals: make(map[ssa.Value]llvm.Value), locals: make(map[ssa.Value]llvm.Value),
blockEntries: make(map[*ssa.BasicBlock]llvm.BasicBlock), blockEntries: make(map[*ssa.BasicBlock]llvm.BasicBlock),
blockExits: make(map[*ssa.BasicBlock]llvm.BasicBlock), blockExits: make(map[*ssa.BasicBlock]llvm.BasicBlock),
blocking: c.ir.IsBlocking(f),
} }
var retType llvm.Type var retType llvm.Type
if frame.blocking { if f.Signature.Results() == nil {
if f.Signature.Results() != nil {
return nil, c.makeError(f.Function.Pos(), "todo: return values in blocking function")
}
retType = c.i8ptrType
} else if f.Signature.Results() == nil {
retType = c.ctx.VoidType() retType = c.ctx.VoidType()
} else if f.Signature.Results().Len() == 1 { } else if f.Signature.Results().Len() == 1 {
var err error var err error
@ -706,9 +656,6 @@ func (c *Compiler) parseFuncDecl(f *ir.Function) (*Frame, error) {
} }
var paramTypes []llvm.Type var paramTypes []llvm.Type
if frame.blocking {
paramTypes = append(paramTypes, c.i8ptrType) // parent coroutine
}
for _, param := range f.Params { for _, param := range f.Params {
paramType, err := c.getLLVMType(param.Type()) paramType, err := c.getLLVMType(param.Type())
if err != nil { if err != nil {
@ -721,7 +668,8 @@ func (c *Compiler) parseFuncDecl(f *ir.Function) (*Frame, error) {
// Add an extra parameter as the function context. This context is used in // Add an extra parameter as the function context. This context is used in
// closures and bound methods, but should be optimized away when not used. // closures and bound methods, but should be optimized away when not used.
if !f.IsExported() { if !f.IsExported() {
paramTypes = append(paramTypes, c.i8ptrType) paramTypes = append(paramTypes, c.i8ptrType) // context
paramTypes = append(paramTypes, c.i8ptrType) // parent coroutine
} }
fnType := llvm.FunctionType(retType, paramTypes, false) fnType := llvm.FunctionType(retType, paramTypes, false)
@ -1095,10 +1043,6 @@ func (c *Compiler) parseFunc(frame *Frame) error {
frame.blockEntries[block] = llvmBlock frame.blockEntries[block] = llvmBlock
frame.blockExits[block] = llvmBlock frame.blockExits[block] = llvmBlock
} }
if frame.blocking {
frame.cleanupBlock = c.ctx.AddBasicBlock(frame.fn.LLVMFn, "task.cleanup")
frame.suspendBlock = c.ctx.AddBasicBlock(frame.fn.LLVMFn, "task.suspend")
}
entryBlock := frame.blockEntries[frame.fn.Blocks[0]] entryBlock := frame.blockEntries[frame.fn.Blocks[0]]
c.builder.SetInsertPointAtEnd(entryBlock) c.builder.SetInsertPointAtEnd(entryBlock)
@ -1137,10 +1081,14 @@ func (c *Compiler) parseFunc(frame *Frame) error {
// Load free variables from the context. This is a closure (or bound // Load free variables from the context. This is a closure (or bound
// method). // method).
if len(frame.fn.FreeVars) != 0 { var context llvm.Value
context := frame.fn.LLVMFn.LastParam() if !frame.fn.IsExported() {
parentHandle := frame.fn.LLVMFn.LastParam()
parentHandle.SetName("parentHandle")
context = llvm.PrevParam(parentHandle)
context.SetName("context") context.SetName("context")
}
if len(frame.fn.FreeVars) != 0 {
// Determine the context type. It's a struct containing all variables. // Determine the context type. It's a struct containing all variables.
freeVarTypes := make([]llvm.Type, 0, len(frame.fn.FreeVars)) freeVarTypes := make([]llvm.Type, 0, len(frame.fn.FreeVars))
for _, freeVar := range frame.fn.FreeVars { for _, freeVar := range frame.fn.FreeVars {
@ -1186,39 +1134,6 @@ func (c *Compiler) parseFunc(frame *Frame) error {
c.deferInitFunc(frame) c.deferInitFunc(frame)
} }
if frame.blocking {
// Coroutine initialization.
taskState := c.builder.CreateAlloca(c.mod.GetTypeByName("runtime.taskState"), "task.state")
stateI8 := c.builder.CreateBitCast(taskState, c.i8ptrType, "task.state.i8")
id := c.builder.CreateCall(c.coroIdFunc, []llvm.Value{
llvm.ConstInt(c.ctx.Int32Type(), 0, false),
stateI8,
llvm.ConstNull(c.i8ptrType),
llvm.ConstNull(c.i8ptrType),
}, "task.token")
size := c.builder.CreateCall(c.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")
} else if c.targetData.TypeAllocSize(size.Type()) < c.targetData.TypeAllocSize(c.uintptrType) {
size = c.builder.CreateZExt(size, c.uintptrType, "task.size.uintptr")
}
data := c.createRuntimeCall("alloc", []llvm.Value{size}, "task.data")
frame.taskHandle = c.builder.CreateCall(c.coroBeginFunc, []llvm.Value{id, data}, "task.handle")
// Coroutine cleanup. Free resources associated with this coroutine.
c.builder.SetInsertPointAtEnd(frame.cleanupBlock)
mem := c.builder.CreateCall(c.coroFreeFunc, []llvm.Value{id, frame.taskHandle}, "task.data.free")
c.createRuntimeCall("free", []llvm.Value{mem}, "")
// re-insert parent coroutine
c.createRuntimeCall("yieldToScheduler", []llvm.Value{frame.fn.LLVMFn.FirstParam()}, "")
c.builder.CreateBr(frame.suspendBlock)
// Coroutine suspend. A call to llvm.coro.suspend() will branch here.
c.builder.SetInsertPointAtEnd(frame.suspendBlock)
c.builder.CreateCall(c.coroEndFunc, []llvm.Value{frame.taskHandle, llvm.ConstInt(c.ctx.Int1Type(), 0, false)}, "unused")
c.builder.CreateRet(frame.taskHandle)
}
// Fill blocks with instructions. // Fill blocks with instructions.
for _, block := range frame.fn.DomPreorder() { for _, block := range frame.fn.DomPreorder() {
if c.DumpSSA { if c.DumpSSA {
@ -1283,25 +1198,38 @@ func (c *Compiler) parseInstr(frame *Frame, instr ssa.Instruction) error {
case *ssa.Defer: case *ssa.Defer:
return c.emitDefer(frame, instr) return c.emitDefer(frame, instr)
case *ssa.Go: case *ssa.Go:
if instr.Common().Method != nil { if instr.Call.IsInvoke() {
return c.makeError(instr.Pos(), "todo: go on method receiver") return c.makeError(instr.Pos(), "todo: go on method receiver")
} }
callee := instr.Call.StaticCallee()
// Execute non-blocking calls (including builtins) directly. if callee == nil {
// parentHandle param is ignored. return c.makeError(instr.Pos(), "todo: go on non-direct function (function pointer, etc.)")
if !c.ir.IsBlocking(c.ir.GetFunction(instr.Common().Value.(*ssa.Function))) {
_, err := c.parseCall(frame, instr.Common(), llvm.Value{})
return err // probably nil
} }
calleeFn := c.ir.GetFunction(callee)
// Start this goroutine. // Mark this function as a 'go' invocation and break invalid
// parentHandle is nil, as the goroutine has no parent frame (it's a new // interprocedural optimizations. For example, heap-to-stack
// stack). // transformations are not sound as goroutines can outlive their parent.
handle, err := c.parseCall(frame, instr.Common(), llvm.Value{}) calleeType := calleeFn.LLVMFn.Type()
calleeValue := c.builder.CreateBitCast(calleeFn.LLVMFn, c.i8ptrType, "")
calleeValue = c.createRuntimeCall("makeGoroutine", []llvm.Value{calleeValue}, "")
calleeValue = c.builder.CreateBitCast(calleeValue, calleeType, "")
// Get all function parameters to pass to the goroutine.
var params []llvm.Value
for _, param := range instr.Call.Args {
val, err := c.parseExpr(frame, param)
if err != nil { if err != nil {
return err return err
} }
c.createRuntimeCall("yieldToScheduler", []llvm.Value{handle}, "") params = append(params, val)
}
if !calleeFn.IsExported() {
params = append(params, llvm.Undef(c.i8ptrType)) // context parameter
params = append(params, llvm.Undef(c.i8ptrType)) // parent coroutine handle
}
c.createCall(calleeValue, params, "")
return nil return nil
case *ssa.If: case *ssa.If:
cond, err := c.parseExpr(frame, instr.Cond) cond, err := c.parseExpr(frame, instr.Cond)
@ -1341,19 +1269,6 @@ func (c *Compiler) parseInstr(frame *Frame, instr ssa.Instruction) error {
c.builder.CreateUnreachable() c.builder.CreateUnreachable()
return nil return nil
case *ssa.Return: case *ssa.Return:
if frame.blocking {
if len(instr.Results) != 0 {
return c.makeError(instr.Pos(), "todo: return values from blocking function")
}
// Final suspend.
continuePoint := c.builder.CreateCall(c.coroSuspendFunc, []llvm.Value{
llvm.ConstNull(c.ctx.TokenType()),
llvm.ConstInt(c.ctx.Int1Type(), 1, false), // final=true
}, "")
sw := c.builder.CreateSwitch(continuePoint, frame.suspendBlock, 2)
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), frame.cleanupBlock)
return nil
} else {
if len(instr.Results) == 0 { if len(instr.Results) == 0 {
c.builder.CreateRetVoid() c.builder.CreateRetVoid()
return nil return nil
@ -1380,7 +1295,6 @@ func (c *Compiler) parseInstr(frame *Frame, instr ssa.Instruction) error {
c.builder.CreateRet(retVal) c.builder.CreateRet(retVal)
return nil return nil
} }
}
case *ssa.RunDefers: case *ssa.RunDefers:
return c.emitRunDefers(frame) return c.emitRunDefers(frame)
case *ssa.Store: case *ssa.Store:
@ -1606,17 +1520,8 @@ func (c *Compiler) parseBuiltin(frame *Frame, args []ssa.Value, callName string,
} }
} }
func (c *Compiler) parseFunctionCall(frame *Frame, args []ssa.Value, llvmFn, context llvm.Value, blocking bool, parentHandle llvm.Value) (llvm.Value, error) { func (c *Compiler) parseFunctionCall(frame *Frame, args []ssa.Value, llvmFn, context llvm.Value, exported bool) (llvm.Value, error) {
var params []llvm.Value var params []llvm.Value
if blocking {
if parentHandle.IsNil() {
// Started from 'go' statement.
params = append(params, llvm.ConstNull(c.i8ptrType))
} else {
// Blocking function calls another blocking function.
params = append(params, parentHandle)
}
}
for _, param := range args { for _, param := range args {
val, err := c.parseExpr(frame, param) val, err := c.parseExpr(frame, param)
if err != nil { if err != nil {
@ -1625,60 +1530,20 @@ func (c *Compiler) parseFunctionCall(frame *Frame, args []ssa.Value, llvmFn, con
params = append(params, val) params = append(params, val)
} }
if !context.IsNil() { if !exported {
// This function takes a context parameter. // This function takes a context parameter.
// Add it to the end of the parameter list. // Add it to the end of the parameter list.
params = append(params, context) params = append(params, context)
// Parent coroutine handle.
params = append(params, llvm.Undef(c.i8ptrType))
} }
if frame.blocking && llvmFn.Name() == "time.Sleep" { return c.createCall(llvmFn, params, ""), nil
// Set task state to TASK_STATE_SLEEP and set the duration.
c.createRuntimeCall("sleepTask", []llvm.Value{frame.taskHandle, params[0]}, "")
// Yield to scheduler.
continuePoint := c.builder.CreateCall(c.coroSuspendFunc, []llvm.Value{
llvm.ConstNull(c.ctx.TokenType()),
llvm.ConstInt(c.ctx.Int1Type(), 0, false),
}, "")
wakeup := c.ctx.InsertBasicBlock(llvm.NextBasicBlock(c.builder.GetInsertBlock()), "task.wakeup")
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)
c.builder.SetInsertPointAtEnd(wakeup)
return llvm.Value{}, nil
} }
result := c.createCall(llvmFn, params, "") func (c *Compiler) parseCall(frame *Frame, instr *ssa.CallCommon) (llvm.Value, error) {
if blocking && !parentHandle.IsNil() {
// Calling a blocking function as a regular function call.
// This is done by passing the current coroutine as a parameter to the
// new coroutine and dropping the current coroutine from the scheduler
// (with the TASK_STATE_CALL state). When the subroutine is finished, it
// will reactivate the parent (this frame) in it's destroy function.
c.createRuntimeCall("yieldToScheduler", []llvm.Value{result}, "")
// Set task state to TASK_STATE_CALL.
c.createRuntimeCall("waitForAsyncCall", []llvm.Value{frame.taskHandle}, "")
// Yield to the scheduler.
continuePoint := c.builder.CreateCall(c.coroSuspendFunc, []llvm.Value{
llvm.ConstNull(c.ctx.TokenType()),
llvm.ConstInt(c.ctx.Int1Type(), 0, false),
}, "")
resume := c.ctx.InsertBasicBlock(llvm.NextBasicBlock(c.builder.GetInsertBlock()), "task.callComplete")
sw := c.builder.CreateSwitch(continuePoint, frame.suspendBlock, 2)
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 0, false), resume)
sw.AddCase(llvm.ConstInt(c.ctx.Int8Type(), 1, false), frame.cleanupBlock)
c.builder.SetInsertPointAtEnd(resume)
}
return result, nil
}
func (c *Compiler) parseCall(frame *Frame, instr *ssa.CallCommon, parentHandle llvm.Value) (llvm.Value, error) {
if instr.IsInvoke() { if instr.IsInvoke() {
// TODO: blocking methods (needs analysis)
fnCast, args, err := c.getInvokeCall(frame, instr) fnCast, args, err := c.getInvokeCall(frame, instr)
if err != nil { if err != nil {
return llvm.Value{}, err return llvm.Value{}, err
@ -1821,7 +1686,7 @@ func (c *Compiler) parseCall(frame *Frame, instr *ssa.CallCommon, parentHandle l
} else { } else {
context = llvm.Undef(c.i8ptrType) context = llvm.Undef(c.i8ptrType)
} }
return c.parseFunctionCall(frame, instr.Args, targetFunc.LLVMFn, context, c.ir.IsBlocking(targetFunc), parentHandle) return c.parseFunctionCall(frame, instr.Args, targetFunc.LLVMFn, context, targetFunc.IsExported())
} }
// Builtin or function pointer. // Builtin or function pointer.
@ -1833,13 +1698,12 @@ func (c *Compiler) parseCall(frame *Frame, instr *ssa.CallCommon, parentHandle l
if err != nil { if err != nil {
return llvm.Value{}, err return llvm.Value{}, err
} }
// TODO: blocking function pointers (needs analysis)
// 'value' is a closure, not a raw function pointer. // 'value' is a closure, not a raw function pointer.
// Extract the function pointer and the context pointer. // Extract the function pointer and the context pointer.
// closure: {context, function pointer} // closure: {context, function pointer}
context := c.builder.CreateExtractValue(value, 0, "") context := c.builder.CreateExtractValue(value, 0, "")
value = c.builder.CreateExtractValue(value, 1, "") value = c.builder.CreateExtractValue(value, 1, "")
return c.parseFunctionCall(frame, instr.Args, value, context, false, parentHandle) return c.parseFunctionCall(frame, instr.Args, value, context, false)
} }
} }
@ -1954,7 +1818,7 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) {
case *ssa.Call: case *ssa.Call:
// Passing the current task here to the subroutine. It is only used when // Passing the current task here to the subroutine. It is only used when
// the subroutine is blocking. // the subroutine is blocking.
return c.parseCall(frame, expr.Common(), frame.taskHandle) return c.parseCall(frame, expr.Common())
case *ssa.ChangeInterface: case *ssa.ChangeInterface:
// Do not change between interface types: always use the underlying // Do not change between interface types: always use the underlying
// (concrete) type in the type number of the interface. Every method // (concrete) type in the type number of the interface. Every method
@ -3129,8 +2993,10 @@ func (c *Compiler) ExternalInt64AsPtr() error {
// Only change externally visible functions (exports and imports). // Only change externally visible functions (exports and imports).
continue continue
} }
if strings.HasPrefix(fn.Name(), "llvm.") { if strings.HasPrefix(fn.Name(), "llvm.") || strings.HasPrefix(fn.Name(), "runtime.") {
// Do not try to modify the signature of internal LLVM functions. // Do not try to modify the signature of internal LLVM functions and
// assume that runtime functions are only temporarily exported for
// coroutine lowering.
continue continue
} }

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

@ -243,6 +243,9 @@ func (c *Compiler) emitRunDefers(frame *Frame) error {
// with a strict calling convention. // with a strict calling convention.
forwardParams = append(forwardParams, llvm.Undef(c.i8ptrType)) forwardParams = append(forwardParams, llvm.Undef(c.i8ptrType))
// Parent coroutine handle.
forwardParams = append(forwardParams, llvm.Undef(c.i8ptrType))
fnPtr, _, err := c.getInvokeCall(frame, callback) fnPtr, _, err := c.getInvokeCall(frame, callback)
if err != nil { if err != nil {
return err return err
@ -277,6 +280,9 @@ func (c *Compiler) emitRunDefers(frame *Frame) error {
// function, but we have to pass one anyway. // function, but we have to pass one anyway.
forwardParams = append(forwardParams, llvm.Undef(c.i8ptrType)) forwardParams = append(forwardParams, llvm.Undef(c.i8ptrType))
// Parent coroutine handle.
forwardParams = append(forwardParams, llvm.Undef(c.i8ptrType))
// Call real function. // Call real function.
c.createCall(callback.LLVMFn, forwardParams, "") c.createCall(callback.LLVMFn, forwardParams, "")
@ -305,6 +311,9 @@ func (c *Compiler) emitRunDefers(frame *Frame) error {
forwardParams = append(forwardParams, forwardParam) forwardParams = append(forwardParams, forwardParam)
} }
// Parent coroutine handle.
forwardParams = append(forwardParams, llvm.Undef(c.i8ptrType))
// Call deferred function. // Call deferred function.
c.createCall(fn.LLVMFn, forwardParams, "") c.createCall(fn.LLVMFn, forwardParams, "")

473
compiler/goroutine-lowering.go Обычный файл
Просмотреть файл

@ -0,0 +1,473 @@
package compiler
// This file lowers goroutine pseudo-functions into coroutines scheduled by a
// scheduler at runtime. It uses coroutine support in LLVM for this
// transformation: https://llvm.org/docs/Coroutines.html
//
// For example, take the following code:
//
// func main() {
// go foo()
// time.Sleep(2 * time.Second)
// println("some other operation")
// bar()
// println("done")
// }
//
// func foo() {
// for {
// println("foo!")
// time.Sleep(time.Second)
// }
// }
//
// func bar() {
// time.Sleep(time.Second)
// println("blocking operation completed)
// }
//
// It is transformed by the IR generator in compiler.go into the following
// pseudo-Go code:
//
// func main() {
// fn := runtime.makeGoroutine(foo)
// fn()
// time.Sleep(2 * time.Second)
// println("some other operation")
// bar() // imagine an 'await' keyword in front of this call
// println("done")
// }
//
// func foo() {
// for {
// println("foo!")
// time.Sleep(time.Second)
// }
// }
//
// func bar() {
// time.Sleep(time.Second)
// println("blocking operation completed)
// }
//
// The pass in this file transforms this code even further, to the following
// async/await style pseudocode:
//
// func main(parent) {
// hdl := llvm.makeCoroutine()
// foo(nil) // do not pass the parent coroutine: this is an independent goroutine
// runtime.sleepTask(hdl, 2 * time.Second) // ask the scheduler to re-activate this coroutine at the right time
// llvm.suspend(hdl) // suspend point
// println("some other operation")
// bar(hdl) // await, pass a continuation (hdl) to bar
// llvm.suspend(hdl) // suspend point, wait for the callee to re-activate
// println("done")
// runtime.activateTask(parent) // re-activate the parent (nop, there is no parent)
// }
//
// func foo(parent) {
// hdl := llvm.makeCoroutine()
// for {
// println("foo!")
// runtime.sleepTask(hdl, time.Second) // ask the scheduler to re-activate this coroutine at the right time
// llvm.suspend(hdl) // suspend point
// }
// }
//
// func bar(parent) {
// hdl := llvm.makeCoroutine()
// runtime.sleepTask(hdl, time.Second) // ask the scheduler to re-activate this coroutine at the right time
// llvm.suspend(hdl) // suspend point
// println("blocking operation completed)
// runtime.activateTask(parent) // re-activate the parent coroutine before returning
// }
//
// The real LLVM code is more complicated, but this is the general idea.
//
// The LLVM coroutine passes will then process this file further transforming
// these three functions into coroutines. Most of the actual work is done by the
// scheduler, which runs in the background scheduling all coroutines.
import (
"errors"
"strings"
"github.com/aykevl/go-llvm"
)
type asyncFunc struct {
taskHandle llvm.Value
cleanupBlock llvm.BasicBlock
suspendBlock llvm.BasicBlock
unreachableBlock llvm.BasicBlock
}
// LowerGoroutines is a pass called during optimization that transforms the IR
// into one where all blocking functions are turned into goroutines and blocking
// calls into await calls.
func (c *Compiler) LowerGoroutines() error {
needsScheduler, err := c.markAsyncFunctions()
if err != nil {
return err
}
uses := getUses(c.mod.NamedFunction("runtime.callMain"))
if len(uses) != 1 || uses[0].IsACallInst().IsNil() {
panic("expected exactly 1 call of runtime.callMain, check the entry point")
}
mainCall := uses[0]
// Replace call of runtime.callMain() with a real call to main.main(),
// 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)}, "")
if needsScheduler {
c.createRuntimeCall("scheduler", nil, "")
}
mainCall.EraseFromParentAsInstruction()
if !needsScheduler {
go_scheduler := c.mod.NamedFunction("go_scheduler")
if !go_scheduler.IsNil() {
// This is the WebAssembly backend.
// There is no need to export the go_scheduler function, but it is
// still exported. Make sure it is optimized away.
go_scheduler.SetLinkage(llvm.InternalLinkage)
}
}
// main.main was set to external linkage during IR construction. Set it to
// internal linkage to enable interprocedural optimizations.
realMain.SetLinkage(llvm.InternalLinkage)
c.mod.NamedFunction("runtime.alloc").SetLinkage(llvm.InternalLinkage)
c.mod.NamedFunction("runtime.free").SetLinkage(llvm.InternalLinkage)
c.mod.NamedFunction("runtime.sleepTask").SetLinkage(llvm.InternalLinkage)
c.mod.NamedFunction("runtime.activateTask").SetLinkage(llvm.InternalLinkage)
c.mod.NamedFunction("runtime.scheduler").SetLinkage(llvm.InternalLinkage)
return nil
}
// 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.
//
// It does the following operations:
// * Find all blocking functions.
// * Determine whether a scheduler is necessary. If not, it skips the
// following operations.
// * Transform call instructions into await calls.
// * Transform return instructions into final suspends.
// * Set up the coroutine frames for async functions.
// * Transform blocking calls into their async equivalents.
func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) {
sleep := c.mod.NamedFunction("time.Sleep")
if sleep.IsNil() {
// There are no blocking operations, so no need to transform anything.
return false, c.lowerMakeGoroutineCalls()
}
// Find all async functions.
// Keep reducing this worklist by marking a function as recursively async
// from the worklist and pushing all its parents that are non-async.
// This is somewhat similar to a worklist in a mark-sweep garbage collector:
// the work items are then grey objects.
worklist := []llvm.Value{sleep}
asyncFuncs := make(map[llvm.Value]*asyncFunc)
asyncList := make([]llvm.Value, 0, 4)
for len(worklist) != 0 {
// Pick the topmost.
f := worklist[len(worklist)-1]
worklist = worklist[:len(worklist)-1]
if _, ok := asyncFuncs[f]; ok {
continue // already processed
}
// Add to set of async functions.
asyncFuncs[f] = &asyncFunc{}
asyncList = append(asyncList, f)
// Add all callees to the worklist.
for _, use := range getUses(f) {
if use.IsConstant() && use.Opcode() == llvm.BitCast {
bitcastUses := getUses(use)
for _, call := range bitcastUses {
if call.IsACallInst().IsNil() || call.CalledValue().Name() != "runtime.makeGoroutine" {
return false, errors.New("async function " + f.Name() + " incorrectly used in bitcast, expected runtime.makeGoroutine")
}
}
// This is a go statement. Do not mark the parent as async, as
// starting a goroutine is not a blocking operation.
continue
}
if use.IsACallInst().IsNil() {
// Not a call instruction. Maybe a store to a global? In any
// case, this requires support for async calls across function
// pointers which is not yet supported.
return false, errors.New("async function " + f.Name() + " used as function pointer")
}
parent := use.InstructionParent().Parent()
for i := 0; i < use.OperandsCount()-1; i++ {
if use.Operand(i) == f {
return false, errors.New("async function " + f.Name() + " used as function pointer in " + parent.Name())
}
}
worklist = append(worklist, parent)
}
}
// 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
} else {
// Only use a scheduler when an async goroutine is started. When the
// goroutine is not async (does not do any blocking operation), no
// scheduler is necessary as it can be called directly.
for _, use := range getUses(makeGoroutine) {
// Input param must be const bitcast of function.
bitcast := use.Operand(0)
if !bitcast.IsConstant() || bitcast.Opcode() != llvm.BitCast {
panic("expected const bitcast operand of runtime.makeGoroutine")
}
goroutine := bitcast.Operand(0)
if _, ok := asyncFuncs[goroutine]; ok {
needsScheduler = true
break
}
}
}
if !needsScheduler {
// 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()
}
// Create a few LLVM intrinsics for coroutine support.
coroIdType := llvm.FunctionType(c.ctx.TokenType(), []llvm.Type{c.ctx.Int32Type(), c.i8ptrType, c.i8ptrType, c.i8ptrType}, false)
coroIdFunc := llvm.AddFunction(c.mod, "llvm.coro.id", coroIdType)
coroSizeType := llvm.FunctionType(c.ctx.Int32Type(), nil, false)
coroSizeFunc := llvm.AddFunction(c.mod, "llvm.coro.size.i32", coroSizeType)
coroBeginType := llvm.FunctionType(c.i8ptrType, []llvm.Type{c.ctx.TokenType(), c.i8ptrType}, false)
coroBeginFunc := llvm.AddFunction(c.mod, "llvm.coro.begin", coroBeginType)
coroSuspendType := llvm.FunctionType(c.ctx.Int8Type(), []llvm.Type{c.ctx.TokenType(), c.ctx.Int1Type()}, false)
coroSuspendFunc := llvm.AddFunction(c.mod, "llvm.coro.suspend", coroSuspendType)
coroEndType := llvm.FunctionType(c.ctx.Int1Type(), []llvm.Type{c.i8ptrType, c.ctx.Int1Type()}, false)
coroEndFunc := llvm.AddFunction(c.mod, "llvm.coro.end", coroEndType)
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.
for _, f := range asyncList {
if f == sleep {
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
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 {
continue
}
asyncCalls = append(asyncCalls, inst)
} else if !inst.IsAReturnInst().IsNil() {
returns = append(returns, inst)
}
}
}
// Coroutine setup.
c.builder.SetInsertPointBefore(f.EntryBasicBlock().FirstInstruction())
taskState := c.builder.CreateAlloca(c.mod.GetTypeByName("runtime.taskState"), "task.state")
stateI8 := c.builder.CreateBitCast(taskState, c.i8ptrType, "task.state.i8")
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")
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")
} else if c.targetData.TypeAllocSize(size.Type()) < c.targetData.TypeAllocSize(c.uintptrType) {
size = c.builder.CreateZExt(size, c.uintptrType, "task.size.uintptr")
}
data := c.createRuntimeCall("alloc", []llvm.Value{size}, "task.data")
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")
// Set task state to TASK_STATE_CALL.
c.builder.SetInsertPointAtEnd(inst.InstructionParent())
// Suspend.
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)
}
// Replace return instructions with suspend points that should
// reactivate the parent coroutine.
for _, inst := range returns {
if inst.OperandsCount() == 0 {
// 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)
}
// 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)
parentHandle := f.LastParam()
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(), 1, 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()
} else {
panic("todo: return value from coroutine")
}
}
// 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")
c.createRuntimeCall("free", []llvm.Value{mem}, "")
c.builder.CreateBr(frame.suspendBlock)
// Coroutine suspend. A call to llvm.coro.suspend() will branch here.
c.builder.SetInsertPointAtEnd(frame.suspendBlock)
c.builder.CreateCall(coroEndFunc, []llvm.Value{frame.taskHandle, llvm.ConstInt(c.ctx.Int1Type(), 0, false)}, "unused")
returnType := f.Type().ElementType().ReturnType()
if returnType.TypeKind() == llvm.VoidTypeKind {
c.builder.CreateRetVoid()
} else {
c.builder.CreateRet(llvm.Undef(returnType))
}
// Coroutine exit. All final suspends (return instructions) will branch
// here.
c.builder.SetInsertPointAtEnd(frame.unreachableBlock)
c.builder.CreateUnreachable()
}
// 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()
}
return true, c.lowerMakeGoroutineCalls()
}
// 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 {
// The following Go code:
// go startedGoroutine()
//
// Is translated to the following during IR construction, to preserve the
// fact that this function should be called as a new goroutine.
// %0 = call i8* @runtime.makeGoroutine(i8* bitcast (void (i8*, i8*)* @main.startedGoroutine to i8*), i8* undef, i8* null)
// %1 = bitcast i8* %0 to void (i8*, i8*)*
// call void %1(i8* undef, i8* undef)
//
// This function rewrites it to a direct call:
// call void @main.startedGoroutine(i8* undef, i8* null)
makeGoroutine := c.mod.NamedFunction("runtime.makeGoroutine")
for _, goroutine := range getUses(makeGoroutine) {
bitcastIn := goroutine.Operand(0)
origFunc := bitcastIn.Operand(0)
uses := getUses(goroutine)
if len(uses) != 1 || uses[0].IsABitCastInst().IsNil() {
return errors.New("expected exactly 1 bitcast use of runtime.makeGoroutine")
}
bitcastOut := uses[0]
uses = getUses(bitcastOut)
if len(uses) != 1 || uses[0].IsACallInst().IsNil() {
return errors.New("expected exactly 1 call use of runtime.makeGoroutine bitcast")
}
realCall := uses[0]
// Create call instruction.
var params []llvm.Value
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)
c.builder.CreateCall(origFunc, params, "")
realCall.EraseFromParentAsInstruction()
bitcastOut.EraseFromParentAsInstruction()
goroutine.EraseFromParentAsInstruction()
}
return nil
}

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

@ -330,6 +330,8 @@ func (c *Compiler) getInvokeCall(frame *Frame, instr *ssa.CallCommon) (llvm.Valu
// Add the context parameter. An interface call never takes a context but we // Add the context parameter. An interface call never takes a context but we
// have to supply the parameter anyway. // have to supply the parameter anyway.
args = append(args, llvm.Undef(c.i8ptrType)) args = append(args, llvm.Undef(c.i8ptrType))
// Add the parent goroutine handle.
args = append(args, llvm.Undef(c.i8ptrType))
return fnCast, args, nil return fnCast, args, nil
} }

95
compiler/llvm.go Обычный файл
Просмотреть файл

@ -0,0 +1,95 @@
package compiler
import (
"github.com/aykevl/go-llvm"
)
// This file contains helper functions for LLVM that are not exposed in the Go
// bindings.
// Return a list of values (actually, instructions) where this value is used as
// an operand.
func getUses(value llvm.Value) []llvm.Value {
if value.IsNil() {
return nil
}
var uses []llvm.Value
use := value.FirstUse()
for !use.IsNil() {
uses = append(uses, use.User())
use = use.NextUse()
}
return uses
}
// splitBasicBlock splits a LLVM basic block into two parts. All instructions
// after afterInst are moved into a new basic block (created right after the
// current one) with the given name.
func (c *Compiler) splitBasicBlock(afterInst llvm.Value, insertAfter llvm.BasicBlock, name string) llvm.BasicBlock {
oldBlock := afterInst.InstructionParent()
newBlock := c.ctx.InsertBasicBlock(insertAfter, name)
var nextInstructions []llvm.Value // values to move
// Collect to-be-moved instructions.
inst := afterInst
for {
inst = llvm.NextInstruction(inst)
if inst.IsNil() {
break
}
nextInstructions = append(nextInstructions, inst)
}
// Move instructions.
c.builder.SetInsertPointAtEnd(newBlock)
for _, inst := range nextInstructions {
inst.RemoveFromParentAsInstruction()
c.builder.Insert(inst)
}
// Find PHI nodes to update.
var phiNodes []llvm.Value // PHI nodes to update
for bb := insertAfter.Parent().FirstBasicBlock(); !bb.IsNil(); bb = llvm.NextBasicBlock(bb) {
for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) {
if inst.IsAPHINode().IsNil() {
continue
}
needsUpdate := false
incomingCount := inst.IncomingCount()
for i := 0; i < incomingCount; i++ {
if inst.IncomingBlock(i) == oldBlock {
needsUpdate = true
break
}
}
if !needsUpdate {
// PHI node has no incoming edge from the old block.
continue
}
phiNodes = append(phiNodes, inst)
}
}
// Update PHI nodes.
for _, phi := range phiNodes {
c.builder.SetInsertPointBefore(phi)
newPhi := c.builder.CreatePHI(phi.Type(), "")
incomingCount := phi.IncomingCount()
incomingVals := make([]llvm.Value, incomingCount)
incomingBlocks := make([]llvm.BasicBlock, incomingCount)
for i := 0; i < incomingCount; i++ {
value := phi.IncomingValue(i)
block := phi.IncomingBlock(i)
if block == oldBlock {
block = newBlock
}
incomingVals[i] = value
incomingBlocks[i] = block
}
newPhi.AddIncoming(incomingVals, incomingBlocks)
phi.ReplaceAllUsesWith(newPhi)
phi.EraseFromParentAsInstruction()
}
return newBlock
}

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

@ -52,9 +52,18 @@ func (c *Compiler) Optimize(optLevel, sizeLevel int, inlinerThreshold uint) erro
// Run TinyGo-specific interprocedural optimizations. // Run TinyGo-specific interprocedural optimizations.
c.OptimizeAllocs() c.OptimizeAllocs()
c.OptimizeStringToBytes() c.OptimizeStringToBytes()
err := c.LowerGoroutines()
if err != nil {
return err
}
} else { } else {
// Must be run at any optimization level. // Must be run at any optimization level.
c.LowerInterfaces() c.LowerInterfaces()
err := c.LowerGoroutines()
if err != nil {
return err
}
} }
if err := c.Verify(); err != nil { if err := c.Verify(); err != nil {
return errors.New("optimizations caused a verification failure") return errors.New("optimizations caused a verification failure")
@ -70,6 +79,13 @@ func (c *Compiler) Optimize(optLevel, sizeLevel int, inlinerThreshold uint) erro
} }
} }
// Run function passes again, because without it, llvm.coro.size.i32()
// doesn't get lowered.
for fn := c.mod.FirstFunction(); !fn.IsNil(); fn = llvm.NextFunction(fn) {
funcPasses.RunFunc(fn)
}
funcPasses.FinalizeFunc()
// Run module passes. // Run module passes.
modPasses := llvm.NewPassManager() modPasses := llvm.NewPassManager()
defer modPasses.Dispose() defer modPasses.Dispose()
@ -324,18 +340,3 @@ func (c *Compiler) hasFlag(call, param llvm.Value, kind string) bool {
} }
return true return true
} }
// Return a list of values (actually, instructions) where this value is used as
// an operand.
func getUses(value llvm.Value) []llvm.Value {
if value.IsNil() {
return nil
}
var uses []llvm.Value
use := value.FirstUse()
for !use.IsNil() {
uses = append(uses, use.User())
use = use.NextUse()
}
return uses
}

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

@ -63,7 +63,7 @@ func Run(mod llvm.Module, targetData llvm.TargetData, debug bool) error {
return errors.New("expected all instructions in " + name + " to be *.init() calls") return errors.New("expected all instructions in " + name + " to be *.init() calls")
} }
pkgName := initName[:len(initName)-5] pkgName := initName[:len(initName)-5]
_, err := e.Function(call.CalledValue(), []Value{&LocalValue{e, undefPtr}}, pkgName) _, err := e.Function(call.CalledValue(), []Value{&LocalValue{e, undefPtr}, &LocalValue{e, undefPtr}}, pkgName)
if err == ErrUnreachable { if err == ErrUnreachable {
break break
} }

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

@ -27,8 +27,6 @@ type Program struct {
globalMap map[*ssa.Global]*Global globalMap map[*ssa.Global]*Global
comments map[string]*ast.CommentGroup comments map[string]*ast.CommentGroup
NamedTypes []*NamedType NamedTypes []*NamedType
needsScheduler bool
goCalls []*ssa.Go
} }
// Function or method. // Function or method.
@ -38,11 +36,8 @@ type Function struct {
linkName string // go:linkname, go:export, go:interrupt linkName string // go:linkname, go:export, go:interrupt
exported bool // go:export exported bool // go:export
nobounds bool // go:nobounds nobounds bool // go:nobounds
blocking bool // calculated by AnalyseBlockingRecursive
flag bool // used by dead code elimination flag bool // used by dead code elimination
interrupt bool // go:interrupt interrupt bool // go:interrupt
parents []*Function // calculated by AnalyseCallgraph
children []*Function // calculated by AnalyseCallgraph
} }
// Global variable, possibly constant. // Global variable, possibly constant.

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

@ -56,110 +56,6 @@ func signature(sig *types.Signature) string {
return s return s
} }
// Fill in parents of all functions.
//
// All packages need to be added before this pass can run, or it will produce
// incorrect results.
func (p *Program) AnalyseCallgraph() {
for _, f := range p.Functions {
// Clear, if AnalyseCallgraph has been called before.
f.children = nil
f.parents = nil
for _, block := range f.Blocks {
for _, instr := range block.Instrs {
switch instr := instr.(type) {
case *ssa.Call:
if instr.Common().IsInvoke() {
continue
}
switch call := instr.Call.Value.(type) {
case *ssa.Builtin:
// ignore
case *ssa.Function:
if isCGoInternal(call.Name()) {
continue
}
child := p.GetFunction(call)
if child.CName() != "" {
continue // assume non-blocking
}
if child.RelString(nil) == "time.Sleep" {
f.blocking = true
}
f.children = append(f.children, child)
}
}
}
}
}
for _, f := range p.Functions {
for _, child := range f.children {
child.parents = append(child.parents, f)
}
}
}
// Analyse which functions are recursively blocking.
//
// Depends on AnalyseCallgraph.
func (p *Program) AnalyseBlockingRecursive() {
worklist := make([]*Function, 0)
// Fill worklist with directly blocking functions.
for _, f := range p.Functions {
if f.blocking {
worklist = append(worklist, f)
}
}
// Keep reducing this worklist by marking a function as recursively blocking
// from the worklist and pushing all its parents that are non-blocking.
// This is somewhat similar to a worklist in a mark-sweep garbage collector.
// The work items are then grey objects.
for len(worklist) != 0 {
// Pick the topmost.
f := worklist[len(worklist)-1]
worklist = worklist[:len(worklist)-1]
for _, parent := range f.parents {
if !parent.blocking {
parent.blocking = true
worklist = append(worklist, parent)
}
}
}
}
// Check whether we need a scheduler. A scheduler is only necessary when there
// are go calls that start blocking functions (if they're not blocking, the go
// function can be turned into a regular function call).
//
// Depends on AnalyseBlockingRecursive.
func (p *Program) AnalyseGoCalls() {
p.goCalls = nil
for _, f := range p.Functions {
for _, block := range f.Blocks {
for _, instr := range block.Instrs {
switch instr := instr.(type) {
case *ssa.Go:
p.goCalls = append(p.goCalls, instr)
}
}
}
}
for _, instr := range p.goCalls {
switch instr := instr.Call.Value.(type) {
case *ssa.Builtin:
case *ssa.Function:
if p.functionMap[instr].blocking {
p.needsScheduler = true
}
default:
panic("unknown go call function type")
}
}
}
// Simple pass that removes dead code. This pass makes later analysis passes // Simple pass that removes dead code. This pass makes later analysis passes
// more useful. // more useful.
func (p *Program) SimpleDCE() { func (p *Program) SimpleDCE() {
@ -239,21 +135,3 @@ func (p *Program) SimpleDCE() {
} }
p.Functions = livefunctions p.Functions = livefunctions
} }
// Whether this function needs a scheduler.
//
// Depends on AnalyseGoCalls.
func (p *Program) NeedsScheduler() bool {
return p.needsScheduler
}
// Whether this function blocks. Builtins are also accepted for convenience.
// They will always be non-blocking.
//
// Depends on AnalyseBlockingRecursive.
func (p *Program) IsBlocking(f *Function) bool {
if !p.needsScheduler {
return false
}
return f.blocking
}

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

@ -10,8 +10,8 @@ const Compiler = "tgo"
// package. // package.
func initAll() func initAll()
// The compiler will insert the call to main.main() here, depending on whether // A function call to this function is replaced withone of the following,
// the scheduler is necessary. // depending on whether the scheduler is necessary:
// //
// Without scheduler: // Without scheduler:
// //
@ -19,9 +19,9 @@ func initAll()
// //
// With scheduler: // With scheduler:
// //
// coroutine := main.main(nil) // main.main()
// scheduler(coroutine) // scheduler()
func mainWrapper() func callMain()
func GOMAXPROCS(n int) int { func GOMAXPROCS(n int) int {
// Note: setting GOMAXPROCS is ignored. // Note: setting GOMAXPROCS is ignored.

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

@ -15,7 +15,7 @@ type timeUnit int64
func main() { func main() {
preinit() preinit()
initAll() initAll()
mainWrapper() callMain()
abort() abort()
} }
@ -238,6 +238,8 @@ type isrFlag bool
var timerWakeup isrFlag var timerWakeup isrFlag
const asyncScheduler = false
// sleepTicks should sleep for d number of microseconds. // sleepTicks should sleep for d number of microseconds.
func sleepTicks(d timeUnit) { func sleepTicks(d timeUnit) {
for d != 0 { for d != 0 {

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

@ -41,7 +41,7 @@ func main() {
preinit() preinit()
initAll() initAll()
postinit() postinit()
mainWrapper() callMain()
abort() abort()
} }
@ -71,6 +71,8 @@ func putchar(c byte) {
machine.UART0.WriteByte(c) machine.UART0.WriteByte(c)
} }
const asyncScheduler = false
// Sleep this number of ticks of 16ms. // Sleep this number of ticks of 16ms.
// //
// TODO: not very accurate. Improve accuracy by calibrating on startup and every // TODO: not very accurate. Improve accuracy by calibrating on startup and every

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

@ -20,7 +20,7 @@ func main() {
systemInit() systemInit()
preinit() preinit()
initAll() initAll()
mainWrapper() callMain()
abort() abort()
} }
@ -50,6 +50,8 @@ func putchar(c byte) {
machine.UART0.WriteByte(c) machine.UART0.WriteByte(c)
} }
const asyncScheduler = false
func sleepTicks(d timeUnit) { func sleepTicks(d timeUnit) {
for d != 0 { for d != 0 {
ticks() // update timestamp ticks() // update timestamp

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

@ -20,11 +20,13 @@ var timestamp timeUnit
func main() { func main() {
preinit() preinit()
initAll() initAll()
mainWrapper() callMain()
arm.SemihostingCall(arm.SemihostingReportException, arm.SemihostingApplicationExit) arm.SemihostingCall(arm.SemihostingReportException, arm.SemihostingApplicationExit)
abort() abort()
} }
const asyncScheduler = false
func sleepTicks(d timeUnit) { func sleepTicks(d timeUnit) {
// TODO: actually sleep here for the given time. // TODO: actually sleep here for the given time.
timestamp += d timestamp += d

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

@ -8,6 +8,6 @@ type timeUnit int64
func main() { func main() {
preinit() preinit()
initAll() initAll()
mainWrapper() callMain()
abort() abort()
} }

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

@ -107,6 +107,8 @@ func initTIM() {
arm.EnableIRQ(stm32.IRQ_TIM3) arm.EnableIRQ(stm32.IRQ_TIM3)
} }
const asyncScheduler = false
// sleepTicks should sleep for specific number of microseconds. // sleepTicks should sleep for specific number of microseconds.
func sleepTicks(d timeUnit) { func sleepTicks(d timeUnit) {
for d != 0 { for d != 0 {

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

@ -46,8 +46,8 @@ func main() int {
// Run initializers of all packages. // Run initializers of all packages.
initAll() initAll()
// Compiler-generated wrapper to main.main(). // Compiler-generated call to main.main().
mainWrapper() callMain()
// For libc compatibility. // For libc compatibility.
return 0 return 0
@ -57,6 +57,8 @@ func putchar(c byte) {
_putchar(int(c)) _putchar(int(c))
} }
const asyncScheduler = false
func sleepTicks(d timeUnit) { func sleepTicks(d timeUnit) {
usleep(uint(d) / 1000) usleep(uint(d) / 1000)
} }

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

@ -6,11 +6,9 @@ import (
"unsafe" "unsafe"
) )
type timeUnit int64 type timeUnit float64 // time in milliseconds, just like Date.now() in JavaScript
const tickMicros = 1 const tickMicros = 1000000
var timestamp timeUnit
//go:export io_get_stdout //go:export io_get_stdout
func io_get_stdout() int32 func io_get_stdout() int32
@ -32,21 +30,27 @@ func _start() {
//go:export cwa_main //go:export cwa_main
func cwa_main() { func cwa_main() {
initAll() // _start is not called by olin/cwa so has to be called here initAll() // _start is not called by olin/cwa so has to be called here
mainWrapper() callMain()
} }
func putchar(c byte) { func putchar(c byte) {
resource_write(stdout, &c, 1) resource_write(stdout, &c, 1)
} }
func sleepTicks(d timeUnit) { //go:export go_scheduler
// TODO: actually sleep here for the given time. func go_scheduler() {
timestamp += d scheduler()
} }
func ticks() timeUnit { const asyncScheduler = true
return timestamp
} // This function is called by the scheduler.
// Schedule a call to runtime.scheduler, do not actually sleep.
//go:export runtime.sleepTicks
func sleepTicks(d timeUnit)
//go:export runtime.ticks
func ticks() timeUnit
// Abort executes the wasm 'unreachable' instruction. // Abort executes the wasm 'unreachable' instruction.
func abort() { func abort() {

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

@ -9,14 +9,13 @@ package runtime
// * A blocking function that calls a non-blocking function is called as // * A blocking function that calls a non-blocking function is called as
// usual. // usual.
// * A blocking function that calls a blocking function passes its own // * A blocking function that calls a blocking function passes its own
// coroutine handle as a parameter to the subroutine and will make sure it's // coroutine handle as a parameter to the subroutine. When the subroutine
// own coroutine is removed from the scheduler. When the subroutine returns, // returns, it will re-insert the parent into the scheduler.
// it will re-insert the parent into the scheduler.
// Note that a goroutine is generally called a 'task' for brevity and because // Note that a goroutine is generally called a 'task' for brevity and because
// that's the more common term among RTOSes. But a goroutine and a task are // that's the more common term among RTOSes. But a goroutine and a task are
// basically the same thing. Although, the code often uses the word 'task' to // basically the same thing. Although, the code often uses the word 'task' to
// refer to both a coroutine and a goroutine, as most of the scheduler isn't // refer to both a coroutine and a goroutine, as most of the scheduler doesn't
// aware of the difference. // care about the difference.
// //
// For more background on coroutines in LLVM: // For more background on coroutines in LLVM:
// https://llvm.org/docs/Coroutines.html // https://llvm.org/docs/Coroutines.html
@ -45,25 +44,19 @@ func (t *coroutine) _promise(alignment int32, from bool) unsafe.Pointer
// Get the promise belonging to a task. // Get the promise belonging to a task.
func (t *coroutine) promise() *taskState { func (t *coroutine) promise() *taskState {
return (*taskState)(t._promise(4, false)) return (*taskState)(t._promise(int32(unsafe.Alignof(taskState{})), false))
} }
func makeGoroutine(*uint8) *uint8
// State/promise of a task. Internally represented as: // State/promise of a task. Internally represented as:
// //
// {i8 state, i32 data, i8* next} // {i8* next, i32/i64 data}
type taskState struct { type taskState struct {
state uint8
data uint32
next *coroutine next *coroutine
data uint
} }
// Various states a task can be in.
const (
TASK_STATE_RUNNABLE = iota
TASK_STATE_SLEEP
TASK_STATE_CALL // waiting for a sub-coroutine
)
// Queues used by the scheduler. // Queues used by the scheduler.
// //
// TODO: runqueueFront can be removed by making the run queue a circular linked // TODO: runqueueFront can be removed by making the run queue a circular linked
@ -89,48 +82,28 @@ func scheduleLogTask(msg string, t *coroutine) {
} }
} }
// Set the task state to sleep for a given time. // Set the task to sleep for a given time.
// //
// This is a compiler intrinsic. // This is a compiler intrinsic.
func sleepTask(caller *coroutine, duration int64) { func sleepTask(caller *coroutine, duration int64) {
if schedulerDebug { if schedulerDebug {
println(" set state sleep:", caller, uint32(duration/tickMicros)) println(" set sleep:", caller, uint(duration/tickMicros))
} }
promise := caller.promise() promise := caller.promise()
promise.state = TASK_STATE_SLEEP promise.data = uint(duration / tickMicros) // TODO: longer durations
promise.data = uint32(duration / tickMicros) // TODO: longer durations addSleepTask(caller)
} }
// Wait for the result of an async call. This means that the parent goroutine // Add a non-queued task to the run queue.
// will be removed from the runqueue and be rescheduled by the callee.
// //
// This is a compiler intrinsic. // This is a compiler intrinsic, and is called from a callee to reactivate the
func waitForAsyncCall(caller *coroutine) { // caller.
scheduleLogTask(" set state call:", caller) func activateTask(task *coroutine) {
promise := caller.promise() if task == nil {
promise.state = TASK_STATE_CALL
}
// Add a task to the runnable or sleep queue, depending on the state.
//
// This is a compiler intrinsic.
func yieldToScheduler(t *coroutine) {
if t == nil {
return return
} }
// See what we should do with this task: try to execute it directly scheduleLogTask(" set runnable:", task)
// again or let it sleep for a bit. runqueuePushBack(task)
promise := t.promise()
if promise.state == TASK_STATE_CALL {
scheduleLogTask(" set waiting for call:", t)
return // calling an async task, the subroutine will re-active the parent
} else if promise.state == TASK_STATE_SLEEP && promise.data != 0 {
scheduleLogTask(" set sleeping:", t)
addSleepTask(t)
} else {
scheduleLogTask(" set runnable:", t)
runqueuePushBack(t)
}
} }
// 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
@ -145,9 +118,6 @@ func runqueuePushBack(t *coroutine) {
if t.promise().next != nil { if t.promise().next != nil {
panic("runtime: runqueuePushBack: expected next task to be nil") panic("runtime: runqueuePushBack: expected next task to be nil")
} }
if t.promise().state != TASK_STATE_RUNNABLE {
panic("runtime: runqueuePushBack: expected task state to be runnable")
}
} }
if runqueueBack == nil { // empty runqueue if runqueueBack == nil { // empty runqueue
scheduleLogTask(" add to runqueue front:", t) scheduleLogTask(" add to runqueue front:", t)
@ -169,10 +139,6 @@ func runqueuePopFront() *coroutine {
} }
if schedulerDebug { if schedulerDebug {
println(" runqueuePopFront:", t) println(" runqueuePopFront:", t)
// Sanity checking.
if t.promise().state != TASK_STATE_RUNNABLE {
panic("runtime: runqueuePopFront: task not runnable")
}
} }
promise := t.promise() promise := t.promise()
runqueueFront = promise.next runqueueFront = promise.next
@ -190,9 +156,6 @@ func addSleepTask(t *coroutine) {
if t.promise().next != nil { if t.promise().next != nil {
panic("runtime: addSleepTask: expected next task to be nil") panic("runtime: addSleepTask: expected next task to be nil")
} }
if t.promise().state != TASK_STATE_SLEEP {
panic("runtime: addSleepTask: task not sleeping")
}
} }
now := ticks() now := ticks()
if sleepQueue == nil { if sleepQueue == nil {
@ -236,11 +199,7 @@ func addSleepTask(t *coroutine) {
} }
// Run the scheduler until all tasks have finished. // Run the scheduler until all tasks have finished.
// It takes an initial task (main.main) to bootstrap. func scheduler() {
func scheduler(main *coroutine) {
// Initial task.
yieldToScheduler(main)
// Main scheduler loop. // Main scheduler loop.
for { for {
scheduleLog("\n schedule") scheduleLog("\n schedule")
@ -254,7 +213,6 @@ func scheduler(main *coroutine) {
promise := t.promise() promise := t.promise()
sleepQueueBaseTime += timeUnit(promise.data) sleepQueueBaseTime += timeUnit(promise.data)
sleepQueue = promise.next sleepQueue = promise.next
promise.state = TASK_STATE_RUNNABLE
promise.next = nil promise.next = nil
runqueuePushBack(t) runqueuePushBack(t)
} }
@ -271,9 +229,15 @@ func scheduler(main *coroutine) {
} }
timeLeft := timeUnit(sleepQueue.promise().data) - (now - sleepQueueBaseTime) timeLeft := timeUnit(sleepQueue.promise().data) - (now - sleepQueueBaseTime)
if schedulerDebug { if schedulerDebug {
println(" sleeping...", sleepQueue, uint32(timeLeft)) println(" sleeping...", sleepQueue, uint(timeLeft))
} }
sleepTicks(timeUnit(timeLeft)) sleepTicks(timeUnit(timeLeft))
if asyncScheduler {
// The sleepTicks function above only sets a timeout at which
// point the scheduler will be called again. It does not really
// sleep.
break
}
continue continue
} }
@ -281,8 +245,5 @@ func scheduler(main *coroutine) {
scheduleLog(" <- runqueuePopFront") scheduleLog(" <- runqueuePopFront")
scheduleLogTask(" run:", t) scheduleLogTask(" run:", t)
t.resume() t.resume()
// Add the just resumed task to the run queue or the sleep queue.
yieldToScheduler(t)
} }
} }

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

@ -213,6 +213,17 @@
} }
}, },
// func ticks() float64
"runtime.ticks": () => {
return timeOrigin + performance.now();
},
// func sleepTicks(timeout float64)
"runtime.sleepTicks": (timeout) => {
// Do not sleep, only reactivate scheduler after the given timeout.
setTimeout(this._inst.exports.go_scheduler, timeout);
},
// func stringVal(value string) ref // func stringVal(value string) ref
"syscall/js.stringVal": (ret_ptr, value_ptr, value_len) => { "syscall/js.stringVal": (ret_ptr, value_ptr, value_len) => {
const s = loadString(value_ptr, value_len); const s = loadString(value_ptr, value_len);

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

@ -9,6 +9,18 @@ func main() {
println("main 2") println("main 2")
time.Sleep(2 * time.Millisecond) time.Sleep(2 * time.Millisecond)
println("main 3") println("main 3")
// Await a blocking call. This must create a new coroutine.
println("wait:")
wait()
println("end waiting")
// 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'
// prefix.
go nowait()
time.Sleep(time.Millisecond)
println("done with non-blocking goroutine")
} }
func sub() { func sub() {
@ -16,3 +28,13 @@ func sub() {
time.Sleep(2 * time.Millisecond) time.Sleep(2 * time.Millisecond)
println("sub 2") println("sub 2")
} }
func wait() {
println(" wait start")
time.Sleep(time.Millisecond)
println(" wait end")
}
func nowait() {
println("non-blocking goroutine")
}

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

@ -3,3 +3,9 @@ sub 1
main 2 main 2
sub 2 sub 2
main 3 main 3
wait:
wait start
wait end
end waiting
non-blocking goroutine
done with non-blocking goroutine