diff --git a/compiler/channel.go b/compiler/channel.go index 144297e8..e04d5896 100644 --- a/compiler/channel.go +++ b/compiler/channel.go @@ -69,7 +69,7 @@ func (c *Compiler) emitChanRecv(frame *Frame, unop *ssa.UnOp) llvm.Value { c.emitLifetimeEnd(valueAllocaCast, valueAllocaSize) if unop.CommaOk { - commaOk := c.createRuntimeCall("getTaskPromiseData", []llvm.Value{coroutine}, "chan.commaOk.wide") + 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, "") @@ -95,7 +95,7 @@ func (c *Compiler) emitSelect(frame *Frame, expr *ssa.Select) llvm.Value { if expr.Blocking { // Blocks forever: // select {} - c.createRuntimeCall("deadlockStub", nil, "") + c.createRuntimeCall("deadlock", nil, "") return llvm.Undef(llvmType) } else { // No-op: diff --git a/compiler/compiler.go b/compiler/compiler.go index 66b2794d..2a5b80a3 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -30,6 +30,20 @@ func init() { // The TinyGo import path. const tinygoPath = "github.com/tinygo-org/tinygo" +// functionsUsedInTransform is a list of function symbols that may be used +// during TinyGo optimization passes so they have to be marked as external +// linkage until all TinyGo passes have finished. +var functionsUsedInTransforms = []string{ + "runtime.alloc", + "runtime.free", + "runtime.sleepTask", + "runtime.setTaskStatePtr", + "runtime.getTaskStatePtr", + "runtime.activateTask", + "runtime.scheduler", + "runtime.startGoroutine", +} + // Configure the compiler. type Config struct { Triple string // LLVM target triple, e.g. x86_64-unknown-linux-gnu (empty string means default) @@ -38,6 +52,7 @@ type Config struct { GOOS string // GOARCH string // GC string // garbage collection strategy + Scheduler string // scheduler implementation ("coroutines" or "tasks") PanicStrategy string // panic strategy ("print" or "trap") CFlags []string // cflags to pass to cgo LDFlags []string // ldflags to pass to cgo @@ -173,6 +188,17 @@ func (c *Compiler) selectGC() string { return "conservative" } +// selectScheduler picks an appropriate scheduler for the target if none was +// given. +func (c *Compiler) selectScheduler() string { + if c.Scheduler != "" { + // A scheduler was specified in the target description. + return c.Scheduler + } + // Fall back to coroutines, which are supported everywhere. + return "coroutines" +} + // 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 { @@ -189,6 +215,7 @@ func (c *Compiler) Compile(mainPath string) []error { if err != nil { return []error{err} } + buildTags := append([]string{"tinygo", "gc." + c.selectGC(), "scheduler." + c.selectScheduler()}, c.BuildTags...) lprogram := &loader.Program{ Build: &build.Context{ GOARCH: c.GOARCH, @@ -198,7 +225,7 @@ func (c *Compiler) Compile(mainPath string) []error { CgoEnabled: true, UseAllFiles: false, Compiler: "gc", // must be one of the recognized compilers - BuildTags: append([]string{"tinygo", "gc." + c.selectGC()}, c.BuildTags...), + BuildTags: buildTags, }, OverlayBuild: &build.Context{ GOARCH: c.GOARCH, @@ -208,7 +235,7 @@ func (c *Compiler) Compile(mainPath string) []error { CgoEnabled: true, UseAllFiles: false, Compiler: "gc", // must be one of the recognized compilers - BuildTags: append([]string{"tinygo", "gc." + c.selectGC()}, c.BuildTags...), + BuildTags: buildTags, }, OverlayPath: func(path string) string { // Return the (overlay) import path when it should be overlaid, and @@ -335,13 +362,15 @@ func (c *Compiler) Compile(mainPath string) []error { // would be optimized away. realMain := c.mod.NamedFunction(c.ir.MainPkg().Pkg.Path() + ".main") realMain.SetLinkage(llvm.ExternalLinkage) // keep alive until goroutine lowering - c.mod.NamedFunction("runtime.alloc").SetLinkage(llvm.ExternalLinkage) - c.mod.NamedFunction("runtime.free").SetLinkage(llvm.ExternalLinkage) - c.mod.NamedFunction("runtime.sleepTask").SetLinkage(llvm.ExternalLinkage) - c.mod.NamedFunction("runtime.setTaskPromisePtr").SetLinkage(llvm.ExternalLinkage) - c.mod.NamedFunction("runtime.getTaskPromisePtr").SetLinkage(llvm.ExternalLinkage) - c.mod.NamedFunction("runtime.activateTask").SetLinkage(llvm.ExternalLinkage) - c.mod.NamedFunction("runtime.scheduler").SetLinkage(llvm.ExternalLinkage) + + // Make sure these functions are kept in tact during TinyGo transformation passes. + for _, name := range functionsUsedInTransforms { + fn := c.mod.NamedFunction(name) + if fn.IsNil() { + continue + } + fn.SetLinkage(llvm.ExternalLinkage) + } // Load some attributes getAttr := func(attrName string) llvm.Attribute { @@ -1041,25 +1070,21 @@ func (c *Compiler) parseInstr(frame *Frame, instr ssa.Instruction) { } calleeFn := c.ir.GetFunction(callee) - // Mark this function as a 'go' invocation and break invalid - // interprocedural optimizations. For example, heap-to-stack - // transformations are not sound as goroutines can outlive their parent. - calleeType := calleeFn.LLVMFn.Type() - calleeValue := c.builder.CreatePtrToInt(calleeFn.LLVMFn, c.uintptrType, "") - calleeValue = c.createRuntimeCall("makeGoroutine", []llvm.Value{calleeValue}, "") - calleeValue = c.builder.CreateIntToPtr(calleeValue, calleeType, "") - // Get all function parameters to pass to the goroutine. var params []llvm.Value for _, param := range instr.Call.Args { params = append(params, c.getValue(frame, param)) } - if !calleeFn.IsExported() { + if !calleeFn.IsExported() && c.selectScheduler() != "tasks" { + // For coroutine scheduling, this is only required when calling an + // external function. + // For tasks, because all params are stored in a single object, no + // unnecessary parameters should be stored anyway. params = append(params, llvm.Undef(c.i8ptrType)) // context parameter params = append(params, llvm.Undef(c.i8ptrType)) // parent coroutine handle } - c.createCall(calleeValue, params, "") + c.emitStartGoroutine(calleeFn.LLVMFn, params) case *ssa.If: cond := c.getValue(frame, instr.Cond) block := instr.Block() diff --git a/compiler/goroutine-lowering.go b/compiler/goroutine-lowering.go index aa054e15..34a1c879 100644 --- a/compiler/goroutine-lowering.go +++ b/compiler/goroutine-lowering.go @@ -1,5 +1,16 @@ package compiler +// This file implements lowering for the goroutine scheduler. There are two +// scheduler implementations, one based on tasks (like RTOSes and the main Go +// runtime) and one based on a coroutine compiler transformation. The task based +// implementation requires very little work from the compiler but is not very +// portable (in particular, it is very hard if not impossible to support on +// WebAssembly). The coroutine based one requires a lot of work by the compiler +// to implement, but can run virtually anywhere with a single scheduler +// implementation. +// +// The below description is for the coroutine based scheduler. +// // 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 @@ -62,7 +73,7 @@ package compiler // llvm.suspend(hdl) // suspend point // println("some other operation") // var i *int // allocate space on the stack for the return value -// runtime.setTaskPromisePtr(hdl, &i) // store return value alloca in our coroutine promise +// runtime.setTaskStatePtr(hdl, &i) // store return value alloca in our coroutine promise // bar(hdl) // await, pass a continuation (hdl) to bar // llvm.suspend(hdl) // suspend point, wait for the callee to re-activate // println("done", *i) @@ -106,10 +117,65 @@ type asyncFunc struct { 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. +// LowerGoroutines performs some IR transformations necessary to support +// goroutines. It does something different based on whether it uses the +// coroutine or the tasks implementation of goroutines, and whether goroutines +// are necessary at all. func (c *Compiler) LowerGoroutines() error { + switch c.selectScheduler() { + case "coroutines": + return c.lowerCoroutines() + case "tasks": + return c.lowerTasks() + default: + panic("unknown scheduler type") + } +} + +// lowerTasks starts the main goroutine and then runs the scheduler. +// This is enough compiler-level transformation for the task-based scheduler. +func (c *Compiler) lowerTasks() error { + 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] + + realMain := c.mod.NamedFunction(c.ir.MainPkg().Pkg.Path() + ".main") + if len(getUses(c.mod.NamedFunction("runtime.startGoroutine"))) != 0 { + // Program needs a scheduler. Start main.main as a goroutine and start + // the scheduler. + realMainWrapper := c.createGoroutineStartWrapper(realMain) + c.builder.SetInsertPointBefore(mainCall) + zero := llvm.ConstInt(c.uintptrType, 0, false) + c.createRuntimeCall("startGoroutine", []llvm.Value{realMainWrapper, zero}, "") + c.createRuntimeCall("scheduler", nil, "") + } else { + // Program doesn't need a scheduler. Call main.main directly. + c.builder.SetInsertPointBefore(mainCall) + params := []llvm.Value{ + llvm.Undef(c.i8ptrType), // unused context parameter + 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() + + // main.main was set to external linkage during IR construction. Set it to + // internal linkage to enable interprocedural optimizations. + realMain.SetLinkage(llvm.InternalLinkage) + + return nil +} + +// lowerCoroutines transforms the IR into one where all blocking functions are +// turned into goroutines and blocking calls into await calls. It also makes +// sure that the first coroutine is started and the coroutine scheduler will be +// run. +func (c *Compiler) lowerCoroutines() error { needsScheduler, err := c.markAsyncFunctions() if err != nil { return err @@ -144,12 +210,6 @@ func (c *Compiler) LowerGoroutines() error { // 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.setTaskPromisePtr").SetLinkage(llvm.InternalLinkage) - c.mod.NamedFunction("runtime.getTaskPromisePtr").SetLinkage(llvm.InternalLinkage) - c.mod.NamedFunction("runtime.scheduler").SetLinkage(llvm.InternalLinkage) return nil } @@ -173,9 +233,9 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) { if !sleep.IsNil() { worklist = append(worklist, sleep) } - deadlockStub := c.mod.NamedFunction("runtime.deadlockStub") - if !deadlockStub.IsNil() { - worklist = append(worklist, deadlockStub) + deadlock := c.mod.NamedFunction("runtime.deadlock") + if !deadlock.IsNil() { + worklist = append(worklist, deadlock) } chanSend := c.mod.NamedFunction("runtime.chanSend") if !chanSend.IsNil() { @@ -300,7 +360,7 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) { // Transform all async functions into coroutines. for _, f := range asyncList { - if f == sleep || f == deadlockStub || f == chanSend || f == chanRecv { + if f == sleep || f == deadlock || f == chanSend || f == chanRecv { continue } @@ -317,7 +377,7 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) { 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 == deadlockStub || callee == chanSend || callee == chanRecv { + if _, ok := asyncFuncs[callee]; !ok || callee == sleep || callee == deadlock || callee == chanSend || callee == chanRecv { continue } asyncCalls = append(asyncCalls, inst) @@ -365,7 +425,7 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) { retvalAlloca = c.builder.CreateAlloca(inst.Type(), "coro.retvalAlloca") c.builder.SetInsertPointBefore(inst) data := c.builder.CreateBitCast(retvalAlloca, c.i8ptrType, "") - c.createRuntimeCall("setTaskPromisePtr", []llvm.Value{frame.taskHandle, data}, "") + c.createRuntimeCall("setTaskStatePtr", []llvm.Value{frame.taskHandle, data}, "") } // Suspend. @@ -403,7 +463,7 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) { var parentHandle llvm.Value if f.Linkage() == llvm.ExternalLinkage { // Exported function. - // Note that getTaskPromisePtr will panic if it is called with + // 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) @@ -423,7 +483,7 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) { // 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("getTaskPromisePtr", []llvm.Value{parentHandle}, "coro.parentData") + 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: @@ -502,9 +562,9 @@ func (c *Compiler) markAsyncFunctions() (needsScheduler bool, err error) { sleepCall.EraseFromParentAsInstruction() } - // Transform calls to runtime.deadlockStub into coroutine suspends (without + // Transform calls to runtime.deadlock into coroutine suspends (without // resume). - for _, deadlockCall := range getUses(deadlockStub) { + for _, deadlockCall := range getUses(deadlock) { // deadlockCall must be a call instruction. frame := asyncFuncs[deadlockCall.InstructionParent().Parent()] diff --git a/compiler/goroutine.go b/compiler/goroutine.go new file mode 100644 index 00000000..85c5e7d8 --- /dev/null +++ b/compiler/goroutine.go @@ -0,0 +1,83 @@ +package compiler + +// This file implements the 'go' keyword to start a new goroutine. See +// goroutine-lowering.go for more details. + +import "tinygo.org/x/go-llvm" + +// emitStartGoroutine starts a new goroutine with the provided function pointer +// and parameters. +// +// Because a go statement doesn't return anything, return undef. +func (c *Compiler) emitStartGoroutine(funcPtr llvm.Value, params []llvm.Value) llvm.Value { + switch c.selectScheduler() { + case "tasks": + paramBundle := c.emitPointerPack(params) + paramBundle = c.builder.CreatePtrToInt(paramBundle, c.uintptrType, "") + + calleeValue := c.createGoroutineStartWrapper(funcPtr) + c.createRuntimeCall("startGoroutine", []llvm.Value{calleeValue, paramBundle}, "") + case "coroutines": + // Mark this function as a 'go' invocation and break invalid + // interprocedural optimizations. For example, heap-to-stack + // transformations are not sound as goroutines can outlive their parent. + calleeType := funcPtr.Type() + calleeValue := c.builder.CreatePtrToInt(funcPtr, c.uintptrType, "") + calleeValue = c.createRuntimeCall("makeGoroutine", []llvm.Value{calleeValue}, "") + calleeValue = c.builder.CreateIntToPtr(calleeValue, calleeType, "") + + c.createCall(calleeValue, params, "") + default: + panic("unreachable") + } + return llvm.Undef(funcPtr.Type().ElementType().ReturnType()) +} + +// createGoroutineStartWrapper creates a wrapper for the task-based +// implementation of goroutines. For example, to call a function like this: +// +// func add(x, y int) int { ... } +// +// It creates a wrapper like this: +// +// func add$gowrapper(ptr *unsafe.Pointer) { +// args := (*struct{ +// x, y int +// })(ptr) +// add(args.x, args.y) +// } +// +// This is useful because the task-based goroutine start implementation only +// allows a single (pointer) argument to the newly started goroutine. Also, it +// ignores the return value because newly started goroutines do not have a +// return value. +func (c *Compiler) createGoroutineStartWrapper(fn llvm.Value) llvm.Value { + if fn.IsAFunction().IsNil() { + panic("todo: goroutine start wrapper for func value") + } + + // See whether this wrapper has already been created. If so, return it. + name := fn.Name() + wrapper := c.mod.NamedFunction(name + "$gowrapper") + if !wrapper.IsNil() { + return c.builder.CreateIntToPtr(wrapper, c.uintptrType, "") + } + + // Save the current position in the IR builder. + currentBlock := c.builder.GetInsertBlock() + defer c.builder.SetInsertPointAtEnd(currentBlock) + + // Create the wrapper. + wrapperType := llvm.FunctionType(c.ctx.VoidType(), []llvm.Type{c.i8ptrType}, false) + wrapper = llvm.AddFunction(c.mod, name+"$gowrapper", wrapperType) + wrapper.SetLinkage(llvm.PrivateLinkage) + wrapper.SetUnnamedAddr(true) + entry := llvm.AddBasicBlock(wrapper, "entry") + c.builder.SetInsertPointAtEnd(entry) + paramTypes := fn.Type().ElementType().ParamTypes() + params := c.emitPointerUnpack(wrapper.Param(0), paramTypes[:len(paramTypes)-2]) + params = append(params, llvm.Undef(c.i8ptrType), llvm.ConstPointerNull(c.i8ptrType)) + c.builder.CreateCall(fn, params, "") + c.builder.CreateRetVoid() + return c.builder.CreatePtrToInt(wrapper, c.uintptrType, "") +} diff --git a/compiler/optimizer.go b/compiler/optimizer.go index 10ae1a8a..d06ce489 100644 --- a/compiler/optimizer.go +++ b/compiler/optimizer.go @@ -101,6 +101,15 @@ func (c *Compiler) Optimize(optLevel, sizeLevel int, inlinerThreshold uint) erro } } + // After TinyGo-specific transforms have finished, undo exporting these functions. + for _, name := range functionsUsedInTransforms { + fn := c.mod.NamedFunction(name) + if fn.IsNil() { + continue + } + fn.SetLinkage(llvm.InternalLinkage) + } + // 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) { diff --git a/main.go b/main.go index 83e5c96e..c7e162df 100644 --- a/main.go +++ b/main.go @@ -47,6 +47,7 @@ type BuildConfig struct { opt string gc string panicStrategy string + scheduler string printIR bool dumpSSA bool debug bool @@ -97,6 +98,10 @@ func Compile(pkgName, outpath string, spec *TargetSpec, config *BuildConfig, act if extraTags := strings.Fields(config.tags); len(extraTags) != 0 { tags = append(tags, extraTags...) } + scheduler := spec.Scheduler + if config.scheduler != "" { + scheduler = config.scheduler + } compilerConfig := compiler.Config{ Triple: spec.Triple, CPU: spec.CPU, @@ -105,6 +110,7 @@ func Compile(pkgName, outpath string, spec *TargetSpec, config *BuildConfig, act GOARCH: spec.GOARCH, GC: config.gc, PanicStrategy: config.panicStrategy, + Scheduler: scheduler, CFlags: cflags, LDFlags: ldflags, ClangHeaders: getClangHeaderPath(root), @@ -618,6 +624,7 @@ func main() { opt := flag.String("opt", "z", "optimization level: 0, 1, 2, s, z") gc := flag.String("gc", "", "garbage collector to use (none, leaking, conservative)") panicStrategy := flag.String("panic", "print", "panic strategy (print, trap)") + scheduler := flag.String("scheduler", "", "which scheduler to use (coroutines, tasks)") printIR := flag.Bool("printir", false, "print LLVM IR") dumpSSA := flag.Bool("dumpssa", false, "dump internal Go SSA") tags := flag.String("tags", "", "a space-separated list of extra build tags") @@ -643,6 +650,7 @@ func main() { opt: *opt, gc: *gc, panicStrategy: *panicStrategy, + scheduler: *scheduler, printIR: *printIR, dumpSSA: *dumpSSA, debug: !*nodebug, diff --git a/src/runtime/chan.go b/src/runtime/chan.go index 578adf29..98d7143b 100644 --- a/src/runtime/chan.go +++ b/src/runtime/chan.go @@ -30,7 +30,7 @@ import ( type channel struct { elementSize uint16 // the size of one value in this channel state chanState - blocked *coroutine + blocked *task } type chanState uint8 @@ -50,40 +50,44 @@ type chanSelectState struct { value unsafe.Pointer } -func deadlockStub() - // chanSend sends a single value over the channel. If this operation can // complete immediately (there is a goroutine waiting for a value), it sends the // value and re-activates both goroutines. If not, it sets itself as waiting on // a value. -func chanSend(sender *coroutine, ch *channel, value unsafe.Pointer) { +func chanSend(sender *task, ch *channel, value unsafe.Pointer) { if ch == nil { // A nil channel blocks forever. Do not scheduler this goroutine again. + chanYield() return } switch ch.state { case chanStateEmpty: - sender.promise().ptr = value + scheduleLogChan(" send: chan is empty ", ch, sender) + sender.state().ptr = value ch.state = chanStateSend ch.blocked = sender + chanYield() case chanStateRecv: + scheduleLogChan(" send: chan in recv mode", ch, sender) receiver := ch.blocked - receiverPromise := receiver.promise() - memcpy(receiverPromise.ptr, value, uintptr(ch.elementSize)) - receiverPromise.data = 1 // commaOk = true - ch.blocked = receiverPromise.next - receiverPromise.next = nil + receiverState := receiver.state() + memcpy(receiverState.ptr, value, uintptr(ch.elementSize)) + receiverState.data = 1 // commaOk = true + ch.blocked = receiverState.next + receiverState.next = nil activateTask(receiver) - activateTask(sender) + reactivateParent(sender) if ch.blocked == nil { ch.state = chanStateEmpty } case chanStateClosed: runtimePanic("send on closed channel") case chanStateSend: - sender.promise().ptr = value - sender.promise().next = ch.blocked + scheduleLogChan(" send: chan in send mode", ch, sender) + sender.state().ptr = value + sender.state().next = ch.blocked ch.blocked = sender + chanYield() } } @@ -91,36 +95,43 @@ func chanSend(sender *coroutine, ch *channel, value unsafe.Pointer) { // sender, it receives the value immediately and re-activates both coroutines. // If not, it sets itself as available for receiving. If the channel is closed, // it immediately activates itself with a zero value as the result. -func chanRecv(receiver *coroutine, ch *channel, value unsafe.Pointer) { +func chanRecv(receiver *task, ch *channel, value unsafe.Pointer) { if ch == nil { // A nil channel blocks forever. Do not scheduler this goroutine again. + chanYield() return } switch ch.state { case chanStateSend: + scheduleLogChan(" recv: chan in send mode", ch, receiver) sender := ch.blocked - senderPromise := sender.promise() - memcpy(value, senderPromise.ptr, uintptr(ch.elementSize)) - receiver.promise().data = 1 // commaOk = true - ch.blocked = senderPromise.next - senderPromise.next = nil - activateTask(receiver) + senderState := sender.state() + memcpy(value, senderState.ptr, uintptr(ch.elementSize)) + receiver.state().data = 1 // commaOk = true + ch.blocked = senderState.next + senderState.next = nil + reactivateParent(receiver) activateTask(sender) if ch.blocked == nil { ch.state = chanStateEmpty } case chanStateEmpty: - receiver.promise().ptr = value + scheduleLogChan(" recv: chan is empty ", ch, receiver) + receiver.state().ptr = value ch.state = chanStateRecv ch.blocked = receiver + chanYield() case chanStateClosed: + scheduleLogChan(" recv: chan is closed ", ch, receiver) memzero(value, uintptr(ch.elementSize)) - receiver.promise().data = 0 // commaOk = false - activateTask(receiver) + receiver.state().data = 0 // commaOk = false + reactivateParent(receiver) case chanStateRecv: - receiver.promise().ptr = value - receiver.promise().next = ch.blocked + scheduleLogChan(" recv: chan in recv mode", ch, receiver) + receiver.state().ptr = value + receiver.state().next = ch.blocked ch.blocked = receiver + chanYield() } } @@ -143,9 +154,9 @@ func chanClose(ch *channel) { runtimePanic("close channel during send") case chanStateRecv: // The receiver must be re-activated with a zero value. - receiverPromise := ch.blocked.promise() - memzero(receiverPromise.ptr, uintptr(ch.elementSize)) - receiverPromise.data = 0 // commaOk = false + receiverState := ch.blocked.state() + memzero(receiverState.ptr, uintptr(ch.elementSize)) + receiverState.data = 0 // commaOk = false activateTask(ch.blocked) ch.state = chanStateClosed ch.blocked = nil @@ -174,10 +185,10 @@ func chanSelect(recvbuf unsafe.Pointer, states []chanSelectState, blocking bool) case chanStateSend: // We can receive immediately. sender := state.ch.blocked - senderPromise := sender.promise() - memcpy(recvbuf, senderPromise.ptr, uintptr(state.ch.elementSize)) - state.ch.blocked = senderPromise.next - senderPromise.next = nil + senderState := sender.state() + memcpy(recvbuf, senderState.ptr, uintptr(state.ch.elementSize)) + state.ch.blocked = senderState.next + senderState.next = nil activateTask(sender) if state.ch.blocked == nil { state.ch.state = chanStateEmpty @@ -193,11 +204,11 @@ func chanSelect(recvbuf unsafe.Pointer, states []chanSelectState, blocking bool) switch state.ch.state { case chanStateRecv: receiver := state.ch.blocked - receiverPromise := receiver.promise() - memcpy(receiverPromise.ptr, state.value, uintptr(state.ch.elementSize)) - receiverPromise.data = 1 // commaOk = true - state.ch.blocked = receiverPromise.next - receiverPromise.next = nil + receiverState := receiver.state() + memcpy(receiverState.ptr, state.value, uintptr(state.ch.elementSize)) + receiverState.data = 1 // commaOk = true + state.ch.blocked = receiverState.next + receiverState.next = nil activateTask(receiver) if state.ch.blocked == nil { state.ch.state = chanStateEmpty diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index 8657b600..c316bf36 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -79,11 +79,6 @@ func memequal(x, y unsafe.Pointer, n uintptr) bool { return true } -//go:linkname sleep time.Sleep -func sleep(d int64) { - sleepTicks(timeUnit(d / tickMicros)) -} - func nanotime() int64 { return int64(ticks()) * tickMicros } diff --git a/src/runtime/runtime_cortexm.go b/src/runtime/runtime_cortexm.go index e6b5d294..55d41d25 100644 --- a/src/runtime/runtime_cortexm.go +++ b/src/runtime/runtime_cortexm.go @@ -40,6 +40,29 @@ func preinit() { } } +// calleeSavedRegs is the list of registers that must be saved and restored when +// switching between tasks. Also see scheduler_cortexm.S that relies on the +// exact layout of this struct. +type calleeSavedRegs struct { + r4 uintptr + r5 uintptr + r6 uintptr + r7 uintptr + r8 uintptr + r9 uintptr + r10 uintptr + r11 uintptr +} + +// prepareStartTask stores fn and args in some callee-saved registers that can +// then be used by the startTask function (implemented in assembly) to set up +// the initial stack pointer and initial argument with the pointer to the object +// with the goroutine start arguments. +func (r *calleeSavedRegs) prepareStartTask(fn, args uintptr) { + r.r4 = fn + r.r5 = args +} + func abort() { // disable all interrupts arm.DisableInterrupts() diff --git a/src/runtime/scheduler.go b/src/runtime/scheduler.go index cd994fd1..c54a2fcb 100644 --- a/src/runtime/scheduler.go +++ b/src/runtime/scheduler.go @@ -1,63 +1,28 @@ package runtime -// This file implements the Go scheduler using coroutines. -// A goroutine contains a whole stack. A coroutine is just a single function. -// How do we use coroutines for goroutines, then? -// * Every function that contains a blocking call (like sleep) is marked -// blocking, and all it's parents (callers) are marked blocking as well -// transitively until the root (main.main or a go statement). -// * A blocking function that calls a non-blocking function is called as -// usual. -// * A blocking function that calls a blocking function passes its own -// coroutine handle as a parameter to the subroutine. When the subroutine -// returns, it will re-insert the parent into the scheduler. -// 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 -// 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 doesn't -// care about the difference. +// This file implements the TinyGo scheduler. This scheduler is a very simple +// cooperative round robin scheduler, with a runqueue that contains a linked +// list of goroutines (tasks) that should be run next, in order of when they +// were added to the queue (first-in, first-out). It also contains a sleep queue +// with sleeping goroutines in order of when they should be re-activated. // -// For more background on coroutines in LLVM: -// https://llvm.org/docs/Coroutines.html +// The scheduler is used both for the coroutine based scheduler and for the task +// based scheduler (see compiler/goroutine-lowering.go for a description). In +// both cases, the 'task' type is used to represent one goroutine. In the case +// of the task based scheduler, it literally is the goroutine itself: a pointer +// to the bottom of the stack where some important fields are kept. In the case +// of the coroutine-based scheduler, it is the coroutine pointer (a *i8 in +// LLVM). -import ( - "unsafe" -) +import "unsafe" const schedulerDebug = false -// A coroutine instance, wrapped here to provide some type safety. The value -// must not be used directly, it is meant to be used as an opaque *i8 in LLVM. -type coroutine uint8 - -//go:export llvm.coro.resume -func (t *coroutine) resume() - -//go:export llvm.coro.destroy -func (t *coroutine) destroy() - -//go:export llvm.coro.done -func (t *coroutine) done() bool - -//go:export llvm.coro.promise -func (t *coroutine) _promise(alignment int32, from bool) unsafe.Pointer - -// Get the promise belonging to a task. -func (t *coroutine) promise() *taskState { - return (*taskState)(t._promise(int32(unsafe.Alignof(taskState{})), false)) -} - -func makeGoroutine(uintptr) uintptr - -// Compiler stub to get the current goroutine. Calls to this function are -// removed in the goroutine lowering pass. -func getCoroutine() *coroutine - -// State/promise of a task. Internally represented as: +// State of a task. Internally represented as: // -// {i8* next, i1 commaOk, i32/i64 data} +// {i8* next, i8* ptr, i32/i64 data} type taskState struct { - next *coroutine + next *task ptr unsafe.Pointer data uint } @@ -67,35 +32,42 @@ type taskState struct { // TODO: runqueueFront can be removed by making the run queue a circular linked // list. The runqueueBack will simply refer to the front in the 'next' pointer. var ( - runqueueFront *coroutine - runqueueBack *coroutine - sleepQueue *coroutine + runqueueFront *task + runqueueBack *task + sleepQueue *task sleepQueueBaseTime timeUnit ) // Simple logging, for debugging. func scheduleLog(msg string) { if schedulerDebug { - println(msg) + println("---", msg) } } // Simple logging with a task pointer, for debugging. -func scheduleLogTask(msg string, t *coroutine) { +func scheduleLogTask(msg string, t *task) { if schedulerDebug { - println(msg, t) + println("---", msg, t) + } +} + +// Simple logging with a channel and task pointer. +func scheduleLogChan(msg string, ch *channel, t *task) { + if schedulerDebug { + println("---", msg, ch, t) } } // Set the task to sleep for a given time. // // This is a compiler intrinsic. -func sleepTask(caller *coroutine, duration int64) { +func sleepTask(caller *task, duration int64) { if schedulerDebug { println(" set sleep:", caller, uint(duration/tickMicros)) } - promise := caller.promise() - promise.data = uint(duration / tickMicros) // TODO: longer durations + state := caller.state() + state.data = uint(duration / tickMicros) // TODO: longer durations addSleepTask(caller) } @@ -103,83 +75,58 @@ func sleepTask(caller *coroutine, duration int64) { // // This is a compiler intrinsic, and is called from a callee to reactivate the // caller. -func activateTask(task *coroutine) { - if task == nil { +func activateTask(t *task) { + if t == nil { return } - scheduleLogTask(" set runnable:", task) - runqueuePushBack(task) + scheduleLogTask(" set runnable:", t) + runqueuePushBack(t) } -// getTaskPromisePtr is a helper function to set the current .ptr field of a -// coroutine promise. -func setTaskPromisePtr(task *coroutine, value unsafe.Pointer) { - task.promise().ptr = value -} - -// getTaskPromisePtr is a helper function to get the current .ptr field from a -// coroutine promise. -func getTaskPromisePtr(task *coroutine) unsafe.Pointer { - if task == nil { - blockingPanic() - } - return task.promise().ptr -} - -// getTaskPromiseData is a helper function to get the current .data field of a -// coroutine promise. -func getTaskPromiseData(task *coroutine) uint { - return task.promise().data +// getTaskStateData is a helper function to get the current .data field of the +// goroutine state. +func getTaskStateData(t *task) uint { + return t.state().data } // Add this task to the end of the run queue. May also destroy the task if it's // done. -func runqueuePushBack(t *coroutine) { - if t.done() { - scheduleLogTask(" destroy task:", t) - t.destroy() - return - } +func runqueuePushBack(t *task) { if schedulerDebug { - if t.promise().next != nil { + if t.state().next != nil { panic("runtime: runqueuePushBack: expected next task to be nil") } } if runqueueBack == nil { // empty runqueue - scheduleLogTask(" add to runqueue front:", t) runqueueBack = t runqueueFront = t } else { - scheduleLogTask(" add to runqueue back:", t) - lastTaskPromise := runqueueBack.promise() - lastTaskPromise.next = t + lastTaskState := runqueueBack.state() + lastTaskState.next = t runqueueBack = t } } // Get a task from the front of the run queue. Returns nil if there is none. -func runqueuePopFront() *coroutine { +func runqueuePopFront() *task { t := runqueueFront if t == nil { return nil } - if schedulerDebug { - println(" runqueuePopFront:", t) - } - promise := t.promise() - runqueueFront = promise.next + state := t.state() + runqueueFront = state.next if runqueueFront == nil { // Runqueue is empty now. runqueueBack = nil } - promise.next = nil + state.next = nil return t } // Add this task to the sleep queue, assuming its state is set to sleeping. -func addSleepTask(t *coroutine) { +func addSleepTask(t *task) { if schedulerDebug { - if t.promise().next != nil { + if t.state().next != nil { panic("runtime: addSleepTask: expected next task to be nil") } } @@ -192,14 +139,14 @@ func addSleepTask(t *coroutine) { return } - // Make sure promise.data is relative to the queue time base. - promise := t.promise() + // Make sure state.data is relative to the queue time base. + state := t.state() // Insert at front of sleep queue. - if promise.data < sleepQueue.promise().data { + if state.data < sleepQueue.state().data { scheduleLog(" -> sleep at start") - sleepQueue.promise().data -= promise.data - promise.next = sleepQueue + sleepQueue.state().data -= state.data + state.next = sleepQueue sleepQueue = t return } @@ -207,20 +154,20 @@ func addSleepTask(t *coroutine) { // Add to sleep queue (in the middle or at the end). queueIndex := sleepQueue for { - promise.data -= queueIndex.promise().data - if queueIndex.promise().next == nil || queueIndex.promise().data > promise.data { - if queueIndex.promise().next == nil { + state.data -= queueIndex.state().data + if queueIndex.state().next == nil || queueIndex.state().data > state.data { + if queueIndex.state().next == nil { scheduleLog(" -> sleep at end") - promise.next = nil + state.next = nil } else { scheduleLog(" -> sleep in middle") - promise.next = queueIndex.promise().next - promise.next.promise().data -= promise.data + state.next = queueIndex.state().next + state.next.state().data -= state.data } - queueIndex.promise().next = t + queueIndex.state().next = t break } - queueIndex = queueIndex.promise().next + queueIndex = queueIndex.state().next } } @@ -228,18 +175,19 @@ func addSleepTask(t *coroutine) { func scheduler() { // Main scheduler loop. for { - scheduleLog("\n schedule") + scheduleLog("") + scheduleLog(" schedule") now := ticks() // Add tasks that are done sleeping to the end of the runqueue so they // will be executed soon. - if sleepQueue != nil && now-sleepQueueBaseTime >= timeUnit(sleepQueue.promise().data) { + if sleepQueue != nil && now-sleepQueueBaseTime >= timeUnit(sleepQueue.state().data) { t := sleepQueue scheduleLogTask(" awake:", t) - promise := t.promise() - sleepQueueBaseTime += timeUnit(promise.data) - sleepQueue = promise.next - promise.next = nil + state := t.state() + sleepQueueBaseTime += timeUnit(state.data) + sleepQueue = state.next + state.next = nil runqueuePushBack(t) } @@ -253,7 +201,7 @@ func scheduler() { scheduleLog(" no tasks left!") return } - timeLeft := timeUnit(sleepQueue.promise().data) - (now - sleepQueueBaseTime) + timeLeft := timeUnit(sleepQueue.state().data) - (now - sleepQueueBaseTime) if schedulerDebug { println(" sleeping...", sleepQueue, uint(timeLeft)) } @@ -268,7 +216,6 @@ func scheduler() { } // Run the given task. - scheduleLog(" <- runqueuePopFront") scheduleLogTask(" run:", t) t.resume() } diff --git a/src/runtime/scheduler_coroutines.go b/src/runtime/scheduler_coroutines.go new file mode 100644 index 00000000..fa8fff6c --- /dev/null +++ b/src/runtime/scheduler_coroutines.go @@ -0,0 +1,95 @@ +// +build scheduler.coroutines + +package runtime + +// This file implements the Go scheduler using coroutines. +// A goroutine contains a whole stack. A coroutine is just a single function. +// How do we use coroutines for goroutines, then? +// * Every function that contains a blocking call (like sleep) is marked +// blocking, and all it's parents (callers) are marked blocking as well +// transitively until the root (main.main or a go statement). +// * A blocking function that calls a non-blocking function is called as +// usual. +// * A blocking function that calls a blocking function passes its own +// coroutine handle as a parameter to the subroutine. When the subroutine +// returns, it will re-insert the parent into the scheduler. +// Note that we use the type 'task' to refer to a coroutine, for compatibility +// with the task-based scheduler. A task type here does not represent the whole +// task, but just the topmost coroutine. For most of the scheduler, this +// difference doesn't matter. +// +// For more background on coroutines in LLVM: +// https://llvm.org/docs/Coroutines.html + +import "unsafe" + +// A coroutine instance, wrapped here to provide some type safety. The value +// must not be used directly, it is meant to be used as an opaque *i8 in LLVM. +type task uint8 + +//go:export llvm.coro.resume +func (t *task) resume() + +//go:export llvm.coro.destroy +func (t *task) destroy() + +//go:export llvm.coro.done +func (t *task) done() bool + +//go:export llvm.coro.promise +func (t *task) _promise(alignment int32, from bool) unsafe.Pointer + +// Get the state belonging to a task. +func (t *task) state() *taskState { + return (*taskState)(t._promise(int32(unsafe.Alignof(taskState{})), false)) +} + +func makeGoroutine(uintptr) uintptr + +// Compiler stub to get the current goroutine. Calls to this function are +// removed in the goroutine lowering pass. +func getCoroutine() *task + +// getTaskStatePtr is a helper function to set the current .ptr field of a +// coroutine promise. +func setTaskStatePtr(t *task, value unsafe.Pointer) { + t.state().ptr = value +} + +// getTaskStatePtr is a helper function to get the current .ptr field from a +// coroutine promise. +func getTaskStatePtr(t *task) unsafe.Pointer { + if t == nil { + blockingPanic() + } + return t.state().ptr +} + +//go:linkname sleep time.Sleep +func sleep(d int64) { + sleepTicks(timeUnit(d / tickMicros)) +} + +// deadlock is called when a goroutine cannot proceed any more, but is in theory +// not exited (so deferred calls won't run). This can happen for example in code +// like this, that blocks forever: +// +// select{} +// +// The coroutine version is implemented directly in the compiler but it needs +// this definition to work. +func deadlock() + +// reactivateParent reactivates the parent goroutine. It is necessary in case of +// the coroutine-based scheduler. +func reactivateParent(t *task) { + activateTask(t) +} + +// chanYield exits the current goroutine. Used in the channel implementation, to +// suspend the current goroutine until it is reactivated by a channel operation +// of a different goroutine. It is a no-op in the coroutine implementation. +func chanYield() { + // Nothing to do here, simply returning from the channel operation also exits + // the goroutine temporarily. +} diff --git a/src/runtime/scheduler_cortexm.S b/src/runtime/scheduler_cortexm.S new file mode 100644 index 00000000..a5672c31 --- /dev/null +++ b/src/runtime/scheduler_cortexm.S @@ -0,0 +1,94 @@ +.section .text.tinygo_startTask +.global tinygo_startTask +.type tinygo_startTask, %function +tinygo_startTask: + // Small assembly stub for starting a goroutine. This is already run on the + // new stack, with the callee-saved registers already loaded. + // Most importantly, r4 contains the pc of the to-be-started function and r5 + // contains the only argument it is given. Multiple arguments are packed + // into one by storing them in a new allocation. + + // Set the first argument of the goroutine start wrapper, which contains all + // the arguments. + mov r0, r5 + + // Branch to the "goroutine start" function. By using blx instead of bx, + // we'll return here instead of tail calling. + blx r4 + + // After return, exit this goroutine. This is a tail call. + bl runtime.Goexit + +.section .text.tinygo_swapTask +.global tinygo_swapTask +.type tinygo_swapTask, %function +tinygo_swapTask: + // r0 = oldTask *task + // r1 = newTask *task + + // This function stores the current register state to a task struct and + // loads the state of another task to replace the current state. Apart from + // saving and restoring all relevant callee-saved registers, it also ends + // with branching to the last program counter (saved as the lr register, to + // follow the ARM calling convention). + + // On pre-Thumb2 CPUs (Cortex-M0 in particular), registers r8-r15 cannot be + // used directly. Only very few operations work on them, such as mov. That's + // why the higher register values are first stored in the temporary register + // r3 when loading/storing them. + + // Store state to old task. It saves the lr instead of the pc, because that + // will be the pc after returning back to the old task (in a different + // invocation of swapTask). + str r4, [r0, #0] + str r5, [r0, #4] + str r6, [r0, #8] + str r7, [r0, #12] + #if defined(__thumb2__) + str r8, [r0, #16] + str r9, [r0, #20] + str r10, [r0, #24] + str r11, [r0, #28] + str sp, [r0, #32] + str lr, [r0, #36] + #else + mov r3, r8 + str r3, [r0, #16] + mov r3, r9 + str r3, [r0, #20] + mov r3, r10 + str r3, [r0, #24] + mov r3, r11 + str r3, [r0, #28] + mov r3, sp + str r3, [r0, #32] + mov r3, lr + str r3, [r0, #36] + #endif + + // Load state from new task and branch to the previous position in the + // program. + ldr r4, [r1, #0] + ldr r5, [r1, #4] + ldr r6, [r1, #8] + ldr r7, [r1, #12] + #if defined(__thumb2__) + ldr r8, [r1, #16] + ldr r9, [r1, #20] + ldr r10, [r1, #24] + ldr r11, [r1, #28] + ldr sp, [r1, #32] + #else + ldr r3, [r1, #16] + mov r8, r3 + ldr r3, [r1, #20] + mov r9, r3 + ldr r3, [r1, #24] + mov r10, r3 + ldr r3, [r1, #28] + mov r11, r3 + ldr r3, [r1, #32] + mov sp, r3 + #endif + ldr r3, [r1, #36] + bx r3 diff --git a/src/runtime/scheduler_tasks.go b/src/runtime/scheduler_tasks.go new file mode 100644 index 00000000..2d833c5e --- /dev/null +++ b/src/runtime/scheduler_tasks.go @@ -0,0 +1,125 @@ +// +build scheduler.tasks + +package runtime + +import "unsafe" + +const stackSize = 1024 + +// Stack canary, to detect a stack overflow. The number is a random number +// generated by random.org. The bit fiddling dance is necessary because +// otherwise Go wouldn't allow the cast to a smaller integer size. +const stackCanary = uintptr(uint64(0x670c1333b83bf575) & uint64(^uintptr(0))) + +var ( + schedulerState = task{canary: stackCanary} + currentTask *task // currently running goroutine, or nil +) + +// This type points to the bottom of the goroutine stack and contains some state +// that must be kept with the task. The last field is a canary, which is +// necessary to make sure that no stack overflow occured when switching tasks. +type task struct { + // The order of fields in this structs must be kept in sync with assembly! + calleeSavedRegs + sp uintptr + pc uintptr + taskState + canary uintptr // used to detect stack overflows +} + +// getCoroutine returns the currently executing goroutine. It is used as an +// intrinsic when compiling channel operations, but is not necessary with the +// task-based scheduler. +func getCoroutine() *task { + return currentTask +} + +// state is a small helper that returns the task state, and is provided for +// compatibility with the coroutine implementation. +//go:inline +func (t *task) state() *taskState { + return &t.taskState +} + +// resume is a small helper that resumes this task until this task switches back +// to the scheduler. +func (t *task) resume() { + currentTask = t + swapTask(&schedulerState, t) + currentTask = nil +} + +// swapTask saves the current state to oldTask (which must contain the current +// task state) and switches to newTask. Note that this function usually does +// return, when another task (perhaps newTask) switches back to the current +// task. +// +// As an additional protection, before switching tasks, it checks whether this +// goroutine has overflowed the stack. +func swapTask(oldTask, newTask *task) { + if oldTask.canary != stackCanary { + runtimePanic("goroutine stack overflow") + } + swapTaskLower(oldTask, newTask) +} + +//go:linkname swapTaskLower tinygo_swapTask +func swapTaskLower(oldTask, newTask *task) + +// Goexit terminates the currently running goroutine. No other goroutines are affected. +// +// Unlike the main Go implementation, no deffered calls will be run. +//export runtime.Goexit +func Goexit() { + // Swap without rescheduling first, effectively exiting the goroutine. + swapTask(currentTask, &schedulerState) +} + +// startTask is a small wrapper function that sets up the first (and only) +// argument to the new goroutine and makes sure it is exited when the goroutine +// finishes. +//go:extern tinygo_startTask +var startTask [0]uint8 + +// startGoroutine starts a new goroutine with the given function pointer and +// argument. It creates a new goroutine stack, prepares it for execution, and +// adds it to the runqueue. +func startGoroutine(fn, args uintptr) { + stack := alloc(stackSize) + t := (*task)(stack) + t.sp = uintptr(stack) + stackSize + t.pc = uintptr(unsafe.Pointer(&startTask)) + t.prepareStartTask(fn, args) + t.canary = stackCanary + scheduleLogTask(" start goroutine:", t) + runqueuePushBack(t) +} + +//go:linkname sleep time.Sleep +func sleep(d int64) { + sleepTask(currentTask, d) + swapTask(currentTask, &schedulerState) +} + +// deadlock is called when a goroutine cannot proceed any more, but is in theory +// not exited (so deferred calls won't run). This can happen for example in code +// like this, that blocks forever: +// +// select{} +func deadlock() { + Goexit() +} + +// reactivateParent reactivates the parent goroutine. It is a no-op for the task +// based scheduler. +func reactivateParent(t *task) { + // Nothing to do here, tasks don't stop automatically. +} + +// chanYield exits the current goroutine. Used in the channel implementation, to +// suspend the current goroutine until it is reactivated by a channel operation +// of a different goroutine. +func chanYield() { + Goexit() +} diff --git a/target.go b/target.go index ac859243..fb73d677 100644 --- a/target.go +++ b/target.go @@ -29,6 +29,7 @@ type TargetSpec struct { GOARCH string `json:"goarch"` BuildTags []string `json:"build-tags"` GC string `json:"gc"` + Scheduler string `json:"scheduler"` Compiler string `json:"compiler"` Linker string `json:"linker"` RTLib string `json:"rtlib"` // compiler runtime library (libgcc, compiler-rt) @@ -64,6 +65,9 @@ func (spec *TargetSpec) copyProperties(spec2 *TargetSpec) { if spec2.GC != "" { spec.GC = spec2.GC } + if spec2.Scheduler != "" { + spec.Scheduler = spec2.Scheduler + } if spec2.Compiler != "" { spec.Compiler = spec2.Compiler } diff --git a/targets/cortex-m.json b/targets/cortex-m.json index e8588e81..0b0c262c 100644 --- a/targets/cortex-m.json +++ b/targets/cortex-m.json @@ -4,6 +4,7 @@ "goarch": "arm", "compiler": "clang", "gc": "conservative", + "scheduler": "tasks", "linker": "ld.lld", "rtlib": "compiler-rt", "cflags": [ @@ -20,7 +21,8 @@ "--gc-sections" ], "extra-files": [ - "src/device/arm/cortexm.s" + "src/device/arm/cortexm.s", + "src/runtime/scheduler_cortexm.S" ], "gdb": "arm-none-eabi-gdb" } diff --git a/testdata/channel.go b/testdata/channel.go index db5b3f29..966a368d 100644 --- a/testdata/channel.go +++ b/testdata/channel.go @@ -27,9 +27,9 @@ func main() { // Test multi-sender. ch = make(chan int) - go fastsender(ch) - go fastsender(ch) - go fastsender(ch) + go fastsender(ch, 10) + go fastsender(ch, 23) + go fastsender(ch, 40) slowreceiver(ch) // Test multi-receiver. @@ -57,9 +57,10 @@ func main() { go fastreceiver(ch) select { case ch <- 5: - println("select one sent") } close(ch) + time.Sleep(time.Millisecond) + println("did send one") // Test select with a single recv operation (transformed into chan recv). select { @@ -124,17 +125,18 @@ func sendComplex(ch chan complex128) { ch <- 7 + 10.5i } -func fastsender(ch chan int) { - ch <- 10 - ch <- 11 +func fastsender(ch chan int, n int) { + ch <- n + ch <- n + 1 } func slowreceiver(ch chan int) { + sum := 0 for i := 0; i < 6; i++ { - n := <-ch - println("got n:", n) + sum += <-ch time.Sleep(time.Microsecond) } + println("sum of n:", sum) } func slowsender(ch chan int) { diff --git a/testdata/channel.txt b/testdata/channel.txt index 2355bd53..3a741d75 100644 --- a/testdata/channel.txt +++ b/testdata/channel.txt @@ -10,12 +10,7 @@ received num: 7 received num: 8 recv from closed channel: 0 false complex128: (+7.000000e+000+1.050000e+001i) -got n: 10 -got n: 11 -got n: 10 -got n: 11 -got n: 10 -got n: 11 +sum of n: 149 sum: 25 sum: 29 sum: 33 @@ -23,8 +18,8 @@ sum(100): 4950 deadlocking select no-op after no-op -select one sent sum: 5 +did send one select one n: 0 select n from chan: 55 select n from closed chan: 0