aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorJaden Weiss <[email protected]>2020-01-03 00:23:37 -0500
committerAyke <[email protected]>2020-03-17 12:16:10 +0100
commit6a50f25a48385fc78397b6f0f1c83650130d8d16 (patch)
tree16745709bcec53d785bd1b17aca718926cd410dd /src
parent2521cacb513bd59e95dc61746e7e0d0d761dcdc4 (diff)
downloadtinygo-6a50f25a48385fc78397b6f0f1c83650130d8d16.tar.gz
tinygo-6a50f25a48385fc78397b6f0f1c83650130d8d16.zip
refactor coroutine lowering and tasks
Diffstat (limited to 'src')
-rw-r--r--src/internal/task/queue.go98
-rw-r--r--src/internal/task/task.go20
-rw-r--r--src/internal/task/task_coroutine.go97
-rw-r--r--src/internal/task/task_none.go29
-rw-r--r--src/internal/task/task_stack.go74
-rw-r--r--src/internal/task/task_stack_cortexm.go83
-rw-r--r--src/runtime/chan.go48
-rw-r--r--src/runtime/runtime.go13
-rw-r--r--src/runtime/runtime_arm7tdmi.go5
-rw-r--r--src/runtime/runtime_atsamd21.go3
-rw-r--r--src/runtime/runtime_atsamd51.go3
-rw-r--r--src/runtime/runtime_cortexm.go23
-rw-r--r--src/runtime/runtime_cortexm_qemu.go3
-rw-r--r--src/runtime/runtime_fe310.go3
-rw-r--r--src/runtime/runtime_nrf.go3
-rw-r--r--src/runtime/runtime_stm32.go3
-rw-r--r--src/runtime/runtime_unix.go5
-rw-r--r--src/runtime/runtime_wasm.go7
-rw-r--r--src/runtime/scheduler.go176
-rw-r--r--src/runtime/scheduler_any.go12
-rw-r--r--src/runtime/scheduler_coroutines.go95
-rw-r--r--src/runtime/scheduler_cortexm.S9
-rw-r--r--src/runtime/scheduler_none.go14
-rw-r--r--src/runtime/scheduler_tasks.go104
24 files changed, 513 insertions, 417 deletions
diff --git a/src/internal/task/queue.go b/src/internal/task/queue.go
new file mode 100644
index 000000000..c86bc596c
--- /dev/null
+++ b/src/internal/task/queue.go
@@ -0,0 +1,98 @@
+package task
+
+const asserts = false
+
+// Queue is a FIFO container of tasks.
+// The zero value is an empty queue.
+type Queue struct {
+ head, tail *Task
+}
+
+// Push a task onto the queue.
+func (q *Queue) Push(t *Task) {
+ if asserts && t.Next != nil {
+ panic("runtime: pushing a task to a queue with a non-nil Next pointer")
+ }
+ if q.tail != nil {
+ q.tail.Next = t
+ }
+ q.tail = t
+ t.Next = nil
+ if q.head == nil {
+ q.head = t
+ }
+}
+
+// Pop a task off of the queue.
+func (q *Queue) Pop() *Task {
+ t := q.head
+ if t == nil {
+ return nil
+ }
+ q.head = t.Next
+ if q.tail == t {
+ q.tail = nil
+ }
+ t.Next = nil
+ return t
+}
+
+// Append pops the contents of another queue and pushes them onto the end of this queue.
+func (q *Queue) Append(other *Queue) {
+ if q.head == nil {
+ q.head = other.head
+ } else {
+ q.tail.Next = other.head
+ }
+ q.tail = other.tail
+ other.head, other.tail = nil, nil
+}
+
+// Stack is a LIFO container of tasks.
+// The zero value is an empty stack.
+// This is slightly cheaper than a queue, so it can be preferable when strict ordering is not necessary.
+type Stack struct {
+ top *Task
+}
+
+// Push a task onto the stack.
+func (s *Stack) Push(t *Task) {
+ if asserts && t.Next != nil {
+ panic("runtime: pushing a task to a stack with a non-nil Next pointer")
+ }
+ s.top, t.Next = t, s.top
+}
+
+// Pop a task off of the stack.
+func (s *Stack) Pop() *Task {
+ t := s.top
+ if t != nil {
+ s.top = t.Next
+ }
+ t.Next = nil
+ return t
+}
+
+// tail follows the chain of tasks.
+// If t is nil, returns nil.
+// Otherwise, returns the task in the chain where the Next field is nil.
+func (t *Task) tail() *Task {
+ if t == nil {
+ return nil
+ }
+ for t.Next != nil {
+ t = t.Next
+ }
+ return t
+}
+
+// Queue moves the contents of the stack into a queue.
+// Elements can be popped from the queue in the same order that they would be popped from the stack.
+func (s *Stack) Queue() Queue {
+ head := s.top
+ s.top = nil
+ return Queue{
+ head: head,
+ tail: head.tail(),
+ }
+}
diff --git a/src/internal/task/task.go b/src/internal/task/task.go
new file mode 100644
index 000000000..57b29eb39
--- /dev/null
+++ b/src/internal/task/task.go
@@ -0,0 +1,20 @@
+package task
+
+import (
+ "unsafe"
+)
+
+// Task is a state of goroutine for scheduling purposes.
+type Task struct {
+ // Next is a field which can be used to make a linked list of tasks.
+ Next *Task
+
+ // Ptr is a field which can be used for storing a pointer.
+ Ptr unsafe.Pointer
+
+ // Data is a field which can be used for storing state information.
+ Data uint
+
+ // state is the underlying running state of the task.
+ state state
+}
diff --git a/src/internal/task/task_coroutine.go b/src/internal/task/task_coroutine.go
new file mode 100644
index 000000000..374bfe11f
--- /dev/null
+++ b/src/internal/task/task_coroutine.go
@@ -0,0 +1,97 @@
+// +build scheduler.coroutines
+
+package task
+
+import (
+ "unsafe"
+)
+
+// rawState is an underlying coroutine state exposed by llvm.coro.
+// This matches *i8 in LLVM.
+type rawState uint8
+
+//go:export llvm.coro.resume
+func (s *rawState) resume()
+
+type state struct{ *rawState }
+
+//go:export llvm.coro.noop
+func noopState() *rawState
+
+// Resume the task until it pauses or completes.
+func (t *Task) Resume() {
+ t.state.resume()
+}
+
+// setState is used by the compiler to set the state of the function at the beginning of a function call.
+// Returns the state of the caller.
+func (t *Task) setState(s *rawState) *rawState {
+ caller := t.state
+ t.state = state{s}
+ return caller.rawState
+}
+
+// returnTo is used by the compiler to return to the state of the caller.
+func (t *Task) returnTo(parent *rawState) {
+ t.state = state{parent}
+ t.returnCurrent()
+}
+
+// returnCurrent is used by the compiler to return to the state of the caller in a case where the state is not replaced.
+func (t *Task) returnCurrent() {
+ scheduleTask(t)
+}
+
+//go:linkname scheduleTask runtime.runqueuePushBack
+func scheduleTask(*Task)
+
+// setReturnPtr is used by the compiler to store the return buffer into the task.
+// This buffer is where the return value of a function that is about to be called will be stored.
+func (t *Task) setReturnPtr(buf unsafe.Pointer) {
+ t.Ptr = buf
+}
+
+// getReturnPtr is used by the compiler to get the return buffer stored into the task.
+// This is called at the beginning of an async function, and the return is stored into this buffer immediately before resuming the caller.
+func (t *Task) getReturnPtr() unsafe.Pointer {
+ return t.Ptr
+}
+
+// createTask returns a new task struct initialized with a no-op state.
+func createTask() *Task {
+ return &Task{
+ state: state{noopState()},
+ }
+}
+
+// start invokes a function in a new goroutine. Calls to this are inserted by the compiler.
+// The created goroutine starts running immediately.
+// This is implemented inside the compiler.
+func start(fn uintptr, args unsafe.Pointer)
+
+// Current returns the current active task.
+// This is implemented inside the compiler.
+func Current() *Task
+
+// Pause suspends the current running task.
+// This is implemented inside the compiler.
+func Pause()
+
+type taskHolder interface {
+ setState(*rawState) *rawState
+ returnTo(*rawState)
+ returnCurrent()
+ setReturnPtr(unsafe.Pointer)
+ getReturnPtr() unsafe.Pointer
+}
+
+// If there are no direct references to the task methods, they will not be discovered by the compiler, and this will trigger a compiler error.
+// Instantiating this interface forces discovery of these methods.
+var _ = taskHolder((*Task)(nil))
+
+func fake() {
+ // Hack to ensure intrinsics are discovered.
+ Current()
+ go func() {}()
+ Pause()
+}
diff --git a/src/internal/task/task_none.go b/src/internal/task/task_none.go
new file mode 100644
index 000000000..31835892c
--- /dev/null
+++ b/src/internal/task/task_none.go
@@ -0,0 +1,29 @@
+// +build scheduler.none
+
+package task
+
+import "unsafe"
+
+//go:linkname runtimePanic runtime.runtimePanic
+func runtimePanic(str string)
+
+func Pause() {
+ runtimePanic("scheduler is disabled")
+}
+
+func Current() *Task {
+ runtimePanic("scheduler is disabled")
+ return nil
+}
+
+//go:noinline
+func start(fn uintptr, args unsafe.Pointer) {
+ // The compiler will error if this is reachable.
+ runtimePanic("scheduler is disabled")
+}
+
+type state struct{}
+
+func (t *Task) Resume() {
+ runtimePanic("scheduler is disabled")
+}
diff --git a/src/internal/task/task_stack.go b/src/internal/task/task_stack.go
new file mode 100644
index 000000000..0bd2f4c29
--- /dev/null
+++ b/src/internal/task/task_stack.go
@@ -0,0 +1,74 @@
+// +build scheduler.tasks
+
+package task
+
+import "unsafe"
+
+//go:linkname runtimePanic runtime.runtimePanic
+func runtimePanic(str string)
+
+// 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)))
+
+// state is a structure which holds a reference to the state of the task.
+// When the task is suspended, the registers are stored onto the stack and the stack pointer is stored into sp.
+type state struct {
+ // sp is the stack pointer of the saved state.
+ // When the task is inactive, the saved registers are stored at the top of the stack.
+ sp uintptr
+
+ // canaryPtr points to the top word of the stack (the lowest address).
+ // This is used to detect stack overflows.
+ // When initializing the goroutine, the stackCanary constant is stored there.
+ // If the stack overflowed, the word will likely no longer equal stackCanary.
+ canaryPtr *uintptr
+}
+
+// currentTask is the current running task, or nil if currently in the scheduler.
+var currentTask *Task
+
+// Current returns the current active task.
+func Current() *Task {
+ return currentTask
+}
+
+// Pause suspends the current task and returns to the scheduler.
+// This function may only be called when running on a goroutine stack, not when running on the system stack or in an interrupt.
+func Pause() {
+ // Check whether the canary (the lowest address of the stack) is still
+ // valid. If it is not, a stack overflow has occured.
+ if *currentTask.state.canaryPtr != stackCanary {
+ runtimePanic("goroutine stack overflow")
+ }
+ currentTask.state.pause()
+}
+
+// Resume the task until it pauses or completes.
+// This may only be called from the scheduler.
+func (t *Task) Resume() {
+ currentTask = t
+ t.state.resume()
+ currentTask = nil
+}
+
+// initialize the state and prepare to call the specified function with the specified argument bundle.
+func (s *state) initialize(fn uintptr, args unsafe.Pointer) {
+ // Create a stack.
+ stack := make([]uintptr, stackSize/unsafe.Sizeof(uintptr(0)))
+
+ // Invoke architecture-specific initialization.
+ s.archInit(stack, fn, args)
+}
+
+//go:linkname runqueuePushBack runtime.runqueuePushBack
+func runqueuePushBack(*Task)
+
+// start creates and starts a new goroutine with the given function and arguments.
+// The new goroutine is scheduled to run later.
+func start(fn uintptr, args unsafe.Pointer) {
+ t := &Task{}
+ t.state.initialize(fn, args)
+ runqueuePushBack(t)
+}
diff --git a/src/internal/task/task_stack_cortexm.go b/src/internal/task/task_stack_cortexm.go
new file mode 100644
index 000000000..b91e5f041
--- /dev/null
+++ b/src/internal/task/task_stack_cortexm.go
@@ -0,0 +1,83 @@
+// +build scheduler.tasks, cortexm
+
+package task
+
+import "unsafe"
+
+const stackSize = 1024
+
+// 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
+
+ pc uintptr
+}
+
+// registers gets a pointer to the registers stored at the top of the stack.
+func (s *state) registers() *calleeSavedRegs {
+ return (*calleeSavedRegs)(unsafe.Pointer(s.sp))
+}
+
+// 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
+
+// archInit runs architecture-specific setup for the goroutine startup.
+func (s *state) archInit(stack []uintptr, fn uintptr, args unsafe.Pointer) {
+ // Set up the stack canary, a random number that should be checked when
+ // switching from the task back to the scheduler. The stack canary pointer
+ // points to the first word of the stack. If it has changed between now and
+ // the next stack switch, there was a stack overflow.
+ s.canaryPtr = &stack[0]
+ *s.canaryPtr = stackCanary
+
+ // Store the initial sp for the startTask function (implemented in assembly).
+ s.sp = uintptr(unsafe.Pointer(&stack[uintptr(len(stack))-(unsafe.Sizeof(calleeSavedRegs{})/unsafe.Sizeof(uintptr(0)))]))
+
+ // Initialize the registers.
+ // These will be popped off of the stack on the first resume of the goroutine.
+ r := s.registers()
+
+ // Start the function at tinygo_startTask (defined in src/runtime/scheduler_cortexm.S).
+ // This assembly code calls a function (passed in r4) with a single argument (passed in r5).
+ // After the function returns, it calls Pause().
+ r.pc = uintptr(unsafe.Pointer(&startTask))
+
+ // Pass the function to call in r4.
+ // This function is a compiler-generated wrapper which loads arguments out of a struct pointer.
+ // See createGoroutineStartWrapper (defined in compiler/goroutine.go) for more information.
+ r.r4 = fn
+
+ // Pass the pointer to the arguments struct in r5.
+ r.r5 = uintptr(args)
+}
+
+func (s *state) resume() {
+ switchToTask(s.sp)
+}
+
+//export tinygo_switchToTask
+func switchToTask(uintptr)
+
+//export tinygo_switchToScheduler
+func switchToScheduler(*uintptr)
+
+func (s *state) pause() {
+ switchToScheduler(&s.sp)
+}
+
+//export tinygo_pause
+func pause() {
+ Pause()
+}
diff --git a/src/runtime/chan.go b/src/runtime/chan.go
index a4cb47484..95243136d 100644
--- a/src/runtime/chan.go
+++ b/src/runtime/chan.go
@@ -24,6 +24,7 @@ package runtime
// element of the receiving coroutine and setting the 'comma-ok' value to false.
import (
+ "internal/task"
"unsafe"
)
@@ -46,7 +47,7 @@ type channelBlockedList struct {
// If this channel operation is not part of a select, then the pointer field of the state holds the data buffer.
// If this channel operation is part of a select, then the pointer field of the state holds the recieve buffer.
// If this channel operation is a receive, then the data field should be set to zero when resuming due to channel closure.
- t *task
+ t *task.Task
// s is a pointer to the channel select state corresponding to this operation.
// This will be nil if and only if this channel operation is not part of a select statement.
@@ -141,24 +142,24 @@ func (ch *channel) resumeRX(ok bool) unsafe.Pointer {
b, ch.blocked = ch.blocked, ch.blocked.next
// get destination pointer
- dst := b.t.state().ptr
+ dst := b.t.Ptr
if !ok {
// the result value is zero
memzero(dst, ch.elementSize)
- b.t.state().data = 0
+ b.t.Data = 0
}
if b.s != nil {
// tell the select op which case resumed
- b.t.state().ptr = unsafe.Pointer(b.s)
+ b.t.Ptr = unsafe.Pointer(b.s)
// detach associated operations
b.detach()
}
// push task onto runqueue
- runqueuePushBack(b.t)
+ runqueue.Push(b.t)
return dst
}
@@ -171,21 +172,21 @@ func (ch *channel) resumeTX() unsafe.Pointer {
b, ch.blocked = ch.blocked, ch.blocked.next
// get source pointer
- src := b.t.state().ptr
+ src := b.t.Ptr
if b.s != nil {
// use state's source pointer
src = b.s.value
// tell the select op which case resumed
- b.t.state().ptr = unsafe.Pointer(b.s)
+ b.t.Ptr = unsafe.Pointer(b.s)
// detach associated operations
b.detach()
}
// push task onto runqueue
- runqueuePushBack(b.t)
+ runqueue.Push(b.t)
return src
}
@@ -424,17 +425,16 @@ func chanSend(ch *channel, value unsafe.Pointer) {
}
// wait for reciever
- sender := getCoroutine()
+ sender := task.Current()
ch.state = chanStateSend
- senderState := sender.state()
- senderState.ptr = value
+ sender.Ptr = value
ch.blocked = &channelBlockedList{
next: ch.blocked,
t: sender,
}
chanDebug(ch)
- yield()
- senderState.ptr = nil
+ task.Pause()
+ sender.Ptr = nil
}
// chanRecv receives a single value over a channel.
@@ -454,18 +454,17 @@ func chanRecv(ch *channel, value unsafe.Pointer) bool {
}
// wait for a value
- receiver := getCoroutine()
+ receiver := task.Current()
ch.state = chanStateRecv
- receiverState := receiver.state()
- receiverState.ptr, receiverState.data = value, 1
+ receiver.Ptr, receiver.Data = value, 1
ch.blocked = &channelBlockedList{
next: ch.blocked,
t: receiver,
}
chanDebug(ch)
- yield()
- ok := receiverState.data == 1
- receiverState.ptr, receiverState.data = nil, 0
+ task.Pause()
+ ok := receiver.Data == 1
+ receiver.Ptr, receiver.Data = nil, 0
return ok
}
@@ -515,7 +514,7 @@ func chanSelect(recvbuf unsafe.Pointer, states []chanSelectState, ops []channelB
for i, v := range states {
ops[i] = channelBlockedList{
next: v.ch.blocked,
- t: getCoroutine(),
+ t: task.Current(),
s: &states[i],
allSelectOps: ops,
}
@@ -547,14 +546,15 @@ func chanSelect(recvbuf unsafe.Pointer, states []chanSelectState, ops []channelB
}
// expose rx buffer
- getCoroutine().state().ptr = recvbuf
- getCoroutine().state().data = 1
+ t := task.Current()
+ t.Ptr = recvbuf
+ t.Data = 1
// wait for one case to fire
- yield()
+ task.Pause()
// figure out which one fired and return the ok value
- return (uintptr(getCoroutine().state().ptr) - uintptr(unsafe.Pointer(&states[0]))) / unsafe.Sizeof(chanSelectState{}), getCoroutine().state().data != 0
+ return (uintptr(t.Ptr) - uintptr(unsafe.Pointer(&states[0]))) / unsafe.Sizeof(chanSelectState{}), t.Data != 0
}
// tryChanSelect is like chanSelect, but it does a non-blocking select operation.
diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go
index 13f3ab3c9..ae9ab665f 100644
--- a/src/runtime/runtime.go
+++ b/src/runtime/runtime.go
@@ -10,17 +10,8 @@ const Compiler = "tinygo"
// package.
func initAll()
-// A function call to this function is replaced with one of the following,
-// depending on whether the scheduler is necessary:
-//
-// Without scheduler:
-//
-// main.main()
-//
-// With scheduler:
-//
-// main.main()
-// scheduler()
+// callMain is a placeholder for the program main function.
+// All references to this are replaced with references to the program main function by the compiler.
func callMain()
func GOMAXPROCS(n int) int {
diff --git a/src/runtime/runtime_arm7tdmi.go b/src/runtime/runtime_arm7tdmi.go
index cff5bca36..cf2ff6818 100644
--- a/src/runtime/runtime_arm7tdmi.go
+++ b/src/runtime/runtime_arm7tdmi.go
@@ -40,7 +40,10 @@ func main() {
initAll()
// Compiler-generated call to main.main().
- callMain()
+ go callMain()
+
+ // Run the scheduler.
+ scheduler()
}
func preinit() {
diff --git a/src/runtime/runtime_atsamd21.go b/src/runtime/runtime_atsamd21.go
index 440cd148e..a3aa6c973 100644
--- a/src/runtime/runtime_atsamd21.go
+++ b/src/runtime/runtime_atsamd21.go
@@ -17,7 +17,8 @@ type timeUnit int64
func main() {
preinit()
initAll()
- callMain()
+ go callMain()
+ scheduler()
abort()
}
diff --git a/src/runtime/runtime_atsamd51.go b/src/runtime/runtime_atsamd51.go
index 7cbcad044..23d91109c 100644
--- a/src/runtime/runtime_atsamd51.go
+++ b/src/runtime/runtime_atsamd51.go
@@ -16,7 +16,8 @@ type timeUnit int64
func main() {
preinit()
initAll()
- callMain()
+ go callMain()
+ scheduler()
abort()
}
diff --git a/src/runtime/runtime_cortexm.go b/src/runtime/runtime_cortexm.go
index 147a3b39f..04b34039a 100644
--- a/src/runtime/runtime_cortexm.go
+++ b/src/runtime/runtime_cortexm.go
@@ -40,29 +40,6 @@ 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/runtime_cortexm_qemu.go b/src/runtime/runtime_cortexm_qemu.go
index f1f95caad..973c20ac2 100644
--- a/src/runtime/runtime_cortexm_qemu.go
+++ b/src/runtime/runtime_cortexm_qemu.go
@@ -21,7 +21,8 @@ var timestamp timeUnit
func main() {
preinit()
initAll()
- callMain()
+ go callMain()
+ scheduler()
arm.SemihostingCall(arm.SemihostingReportException, arm.SemihostingApplicationExit)
abort()
}
diff --git a/src/runtime/runtime_fe310.go b/src/runtime/runtime_fe310.go
index 92cb9ad17..88295c9c0 100644
--- a/src/runtime/runtime_fe310.go
+++ b/src/runtime/runtime_fe310.go
@@ -52,7 +52,8 @@ func main() {
preinit()
initPeripherals()
initAll()
- callMain()
+ go callMain()
+ scheduler()
abort()
}
diff --git a/src/runtime/runtime_nrf.go b/src/runtime/runtime_nrf.go
index 50d3e5f2b..9b01ded70 100644
--- a/src/runtime/runtime_nrf.go
+++ b/src/runtime/runtime_nrf.go
@@ -22,7 +22,8 @@ func main() {
systemInit()
preinit()
initAll()
- callMain()
+ go callMain()
+ scheduler()
abort()
}
diff --git a/src/runtime/runtime_stm32.go b/src/runtime/runtime_stm32.go
index 0717e7cff..11e686f06 100644
--- a/src/runtime/runtime_stm32.go
+++ b/src/runtime/runtime_stm32.go
@@ -8,6 +8,7 @@ type timeUnit int64
func main() {
preinit()
initAll()
- callMain()
+ go callMain()
+ scheduler()
abort()
}
diff --git a/src/runtime/runtime_unix.go b/src/runtime/runtime_unix.go
index 63d78917e..a88f243f7 100644
--- a/src/runtime/runtime_unix.go
+++ b/src/runtime/runtime_unix.go
@@ -52,7 +52,10 @@ func main() int {
initAll()
// Compiler-generated call to main.main().
- callMain()
+ go callMain()
+
+ // Run scheduler.
+ scheduler()
// For libc compatibility.
return 0
diff --git a/src/runtime/runtime_wasm.go b/src/runtime/runtime_wasm.go
index 09216cc3f..c01fa9326 100644
--- a/src/runtime/runtime_wasm.go
+++ b/src/runtime/runtime_wasm.go
@@ -21,7 +21,8 @@ func fd_write(id uint32, iovs *wasiIOVec, iovs_len uint, nwritten *uint) (errno
//export _start
func _start() {
initAll()
- callMain()
+ go callMain()
+ scheduler()
}
// Using global variables to avoid heap allocation.
@@ -50,7 +51,9 @@ func setEventHandler(fn func()) {
//go:export resume
func resume() {
- handleEvent()
+ go func() {
+ handleEvent()
+ }()
}
//go:export go_scheduler
diff --git a/src/runtime/scheduler.go b/src/runtime/scheduler.go
index 7dc2b7906..49cf8e402 100644
--- a/src/runtime/scheduler.go
+++ b/src/runtime/scheduler.go
@@ -14,27 +14,16 @@ package runtime
// of the coroutine-based scheduler, it is the coroutine pointer (a *i8 in
// LLVM).
-import "unsafe"
+import (
+ "internal/task"
+)
const schedulerDebug = false
-// State of a task. Internally represented as:
-//
-// {i8* next, i8* ptr, i32/i64 data}
-type taskState struct {
- next *task
- ptr unsafe.Pointer
- data uint
-}
-
// Queues used by the scheduler.
-//
-// 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 *task
- runqueueBack *task
- sleepQueue *task
+ runqueue task.Queue
+ sleepQueue *task.Task
sleepQueueBaseTime timeUnit
)
@@ -46,14 +35,14 @@ func scheduleLog(msg string) {
}
// Simple logging with a task pointer, for debugging.
-func scheduleLogTask(msg string, t *task) {
+func scheduleLogTask(msg string, t *task.Task) {
if schedulerDebug {
println("---", msg, t)
}
}
// Simple logging with a channel and task pointer.
-func scheduleLogChan(msg string, ch *channel, t *task) {
+func scheduleLogChan(msg string, ch *channel, t *task.Task) {
if schedulerDebug {
println("---", msg, ch, t)
}
@@ -67,7 +56,7 @@ func scheduleLogChan(msg string, ch *channel, t *task) {
//go:noinline
func deadlock() {
// call yield without requesting a wakeup
- yield()
+ task.Pause()
panic("unreachable")
}
@@ -80,122 +69,20 @@ func Goexit() {
deadlock()
}
-// unblock unblocks a task and returns the next value
-func unblock(t *task) *task {
- state := t.state()
- next := state.next
- state.next = nil
- activateTask(t)
- return next
-}
-
-// unblockChain unblocks the next task on the stack/queue, returning it
-// also updates the chain, putting the next element into the chain pointer
-// if the chain is used as a queue, tail is used as a pointer to the final insertion point
-// if the chain is used as a stack, tail should be nil
-func unblockChain(chain **task, tail ***task) *task {
- t := *chain
- if t == nil {
- return nil
- }
- *chain = unblock(t)
- if tail != nil && *chain == nil {
- *tail = chain
- }
- return t
-}
-
-// dropChain drops a task from the given stack or queue
-// if the chain is used as a queue, tail is used as a pointer to the field containing a pointer to the next insertion point
-// if the chain is used as a stack, tail should be nil
-func dropChain(t *task, chain **task, tail ***task) {
- for c := chain; *c != nil; c = &((*c).state().next) {
- if *c == t {
- next := (*c).state().next
- if next == nil && tail != nil {
- *tail = c
- }
- *c = next
- return
- }
- }
- panic("runtime: task not in chain")
-}
-
-// Pause the current task for a given time.
-//go:linkname sleep time.Sleep
-func sleep(duration int64) {
- addSleepTask(getCoroutine(), duration)
- yield()
-}
-
-func avrSleep(duration int64) {
- sleepTicks(timeUnit(duration / tickMicros))
-}
-
-// Add a non-queued task to the run queue.
-//
-// This is a compiler intrinsic, and is called from a callee to reactivate the
-// caller.
-func activateTask(t *task) {
- if t == nil {
- return
- }
- scheduleLogTask(" set runnable:", t)
- runqueuePushBack(t)
-}
-
-// getTaskStateData is a helper function to get the current .data field of the
-// goroutine state.
-//go:inline
-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 *task) {
- if schedulerDebug {
- scheduleLogTask(" pushing back:", t)
- if t.state().next != nil {
- panic("runtime: runqueuePushBack: expected next task to be nil")
- }
- }
- if runqueueBack == nil { // empty runqueue
- runqueueBack = t
- runqueueFront = t
- } else {
- 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() *task {
- t := runqueueFront
- if t == nil {
- return nil
- }
- state := t.state()
- runqueueFront = state.next
- if runqueueFront == nil {
- // Runqueue is empty now.
- runqueueBack = nil
- }
- state.next = nil
- return t
+// Add this task to the end of the run queue.
+func runqueuePushBack(t *task.Task) {
+ runqueue.Push(t)
}
// Add this task to the sleep queue, assuming its state is set to sleeping.
-func addSleepTask(t *task, duration int64) {
+func addSleepTask(t *task.Task, duration int64) {
if schedulerDebug {
println(" set sleep:", t, uint(duration/tickMicros))
- if t.state().next != nil {
+ if t.Next != nil {
panic("runtime: addSleepTask: expected next task to be nil")
}
}
- t.state().data = uint(duration / tickMicros) // TODO: longer durations
+ t.Data = uint(duration / tickMicros) // TODO: longer durations
now := ticks()
if sleepQueue == nil {
scheduleLog(" -> sleep new queue")
@@ -206,20 +93,20 @@ func addSleepTask(t *task, duration int64) {
// Add to sleep queue.
q := &sleepQueue
- for ; *q != nil; q = &((*q).state()).next {
- if t.state().data < (*q).state().data {
+ for ; *q != nil; q = &(*q).Next {
+ if t.Data < (*q).Data {
// this will finish earlier than the next - insert here
break
} else {
// this will finish later - adjust delay
- t.state().data -= (*q).state().data
+ t.Data -= (*q).Data
}
}
if *q != nil {
// cut delay time between this sleep task and the next
- (*q).state().data -= t.state().data
+ (*q).Data -= t.Data
}
- t.state().next = *q
+ t.Next = *q
*q = t
}
@@ -236,17 +123,16 @@ func scheduler() {
// 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.state().data) {
+ if sleepQueue != nil && now-sleepQueueBaseTime >= timeUnit(sleepQueue.Data) {
t := sleepQueue
scheduleLogTask(" awake:", t)
- state := t.state()
- sleepQueueBaseTime += timeUnit(state.data)
- sleepQueue = state.next
- state.next = nil
- runqueuePushBack(t)
+ sleepQueueBaseTime += timeUnit(t.Data)
+ sleepQueue = t.Next
+ t.Next = nil
+ runqueue.Push(t)
}
- t := runqueuePopFront()
+ t := runqueue.Pop()
if t == nil {
if sleepQueue == nil {
// No more tasks to execute.
@@ -256,11 +142,11 @@ func scheduler() {
scheduleLog(" no tasks left!")
return
}
- timeLeft := timeUnit(sleepQueue.state().data) - (now - sleepQueueBaseTime)
+ timeLeft := timeUnit(sleepQueue.Data) - (now - sleepQueueBaseTime)
if schedulerDebug {
println(" sleeping...", sleepQueue, uint(timeLeft))
- for t := sleepQueue; t != nil; t = t.state().next {
- println(" task sleeping:", t, timeUnit(t.state().data))
+ for t := sleepQueue; t != nil; t = t.Next {
+ println(" task sleeping:", t, timeUnit(t.Data))
}
}
sleepTicks(timeLeft)
@@ -275,11 +161,11 @@ func scheduler() {
// Run the given task.
scheduleLogTask(" run:", t)
- t.resume()
+ t.Resume()
}
}
func Gosched() {
- runqueuePushBack(getCoroutine())
- yield()
+ runqueue.Push(task.Current())
+ task.Pause()
}
diff --git a/src/runtime/scheduler_any.go b/src/runtime/scheduler_any.go
new file mode 100644
index 000000000..d541dc6a3
--- /dev/null
+++ b/src/runtime/scheduler_any.go
@@ -0,0 +1,12 @@
+// +build !scheduler.none
+
+package runtime
+
+import "internal/task"
+
+// Pause the current task for a given time.
+//go:linkname sleep time.Sleep
+func sleep(duration int64) {
+ addSleepTask(task.Current(), duration)
+ task.Pause()
+}
diff --git a/src/runtime/scheduler_coroutines.go b/src/runtime/scheduler_coroutines.go
index 1d0996d94..5e871a64a 100644
--- a/src/runtime/scheduler_coroutines.go
+++ b/src/runtime/scheduler_coroutines.go
@@ -2,103 +2,8 @@
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
-
-// setTaskStatePtr 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
-}
-
-// yield suspends execution of the current goroutine
-// any wakeups must be configured before calling yield
-func yield()
-
// getSystemStackPointer returns the current stack pointer of the system stack.
// This is always the current stack pointer.
func getSystemStackPointer() uintptr {
return getCurrentStackPointer()
}
-
-func fakeCoroutine(dst **task) {
- *dst = getCoroutine()
- for {
- yield()
- }
-}
-
-func getFakeCoroutine() *task {
- // this isnt defined behavior, but this is what our implementation does
- // this is really a horrible hack
- var t *task
- go fakeCoroutine(&t)
-
- // the first line of fakeCoroutine will have completed by now
- return t
-}
-
-// noret is a placeholder that can be used to indicate that an async function is not going to directly return here
-func noret()
-
-func getParentHandle() *task
-
-func llvmCoroRefHolder() {
- noret()
- getParentHandle()
- getCoroutine()
-}
diff --git a/src/runtime/scheduler_cortexm.S b/src/runtime/scheduler_cortexm.S
index 6d83ba4f3..121992e7d 100644
--- a/src/runtime/scheduler_cortexm.S
+++ b/src/runtime/scheduler_cortexm.S
@@ -17,7 +17,7 @@ tinygo_startTask:
blx r4
// After return, exit this goroutine. This is a tail call.
- bl runtime.yield
+ bl tinygo_pause
.section .text.tinygo_getSystemStackPointer
.global tinygo_getSystemStackPointer
@@ -35,24 +35,23 @@ tinygo_getSystemStackPointer:
.global tinygo_switchToScheduler
.type tinygo_switchToScheduler, %function
tinygo_switchToScheduler:
- // r0 = oldTask *task
+ // r0 = sp *uintptr
// Currently on the task stack (SP=PSP). We need to store the position on
// the stack where the in-use registers will be stored.
mov r1, sp
subs r1, #36
- str r1, [r0, #36]
+ str r1, [r0]
b tinygo_swapTask
.global tinygo_switchToTask
.type tinygo_switchToTask, %function
tinygo_switchToTask:
- // r0 = newTask *task
+ // r0 = sp uintptr
// Currently on the scheduler stack (SP=MSP). We'll have to update the PSP,
// and then we can invoke swapTask.
- ldr r0, [r0, #36]
msr PSP, r0
// Continue executing in the swapTask function, which swaps the stack
diff --git a/src/runtime/scheduler_none.go b/src/runtime/scheduler_none.go
new file mode 100644
index 000000000..222867dd0
--- /dev/null
+++ b/src/runtime/scheduler_none.go
@@ -0,0 +1,14 @@
+// +build scheduler.none
+
+package runtime
+
+//go:linkname sleep time.Sleep
+func sleep(duration int64) {
+ sleepTicks(timeUnit(duration / tickMicros))
+}
+
+// getSystemStackPointer returns the current stack pointer of the system stack.
+// This is always the current stack pointer.
+func getSystemStackPointer() uintptr {
+ return getCurrentStackPointer()
+}
diff --git a/src/runtime/scheduler_tasks.go b/src/runtime/scheduler_tasks.go
index b2142892e..9be63d5d5 100644
--- a/src/runtime/scheduler_tasks.go
+++ b/src/runtime/scheduler_tasks.go
@@ -2,110 +2,6 @@
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 (
- 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
- pc uintptr
- sp uintptr
- taskState
- canaryPtr *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.
-//go:inline
-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
- switchToTask(t)
- currentTask = nil
-}
-
-// switchToScheduler saves the current state on the stack, saves the current
-// stack pointer in the task, and switches to the scheduler. It must only be
-// called when actually running on this task.
-// When it returns, the scheduler has switched back to this task (for example,
-// after a blocking operation completed).
-//export tinygo_switchToScheduler
-func switchToScheduler(t *task)
-
-// switchToTask switches from the scheduler to the task. It must only be called
-// from the scheduler.
-// When this function returns, the task just yielded control back to the
-// scheduler.
-//export tinygo_switchToTask
-func switchToTask(t *task)
-
-// 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)(unsafe.Pointer(uintptr(stack) + stackSize - unsafe.Sizeof(task{})))
-
- // Set up the stack canary, a random number that should be checked when
- // switching from the task back to the scheduler. The stack canary pointer
- // points to the first word of the stack. If it has changed between now and
- // the next stack switch, there was a stack overflow.
- t.canaryPtr = (*uintptr)(unsafe.Pointer(stack))
- *t.canaryPtr = stackCanary
-
- // Store the initial sp/pc for the startTask function (implemented in
- // assembly).
- t.sp = uintptr(stack) + stackSize - unsafe.Sizeof(task{})
- t.pc = uintptr(unsafe.Pointer(&startTask))
- t.prepareStartTask(fn, args)
- scheduleLogTask(" start goroutine:", t)
- runqueuePushBack(t)
-}
-
-// yield suspends execution of the current goroutine
-// any wakeups must be configured before calling yield
-//export runtime.yield
-func yield() {
- // Check whether the canary (the lowest address of the stack) is still
- // valid. If it is not, a stack overflow has occured.
- if *currentTask.canaryPtr != stackCanary {
- runtimePanic("goroutine stack overflow")
- }
- switchToScheduler(currentTask)
-}
-
// getSystemStackPointer returns the current stack pointer of the system stack.
// This is not necessarily the same as the current stack pointer.
//export tinygo_getSystemStackPointer