summary refs log tree commit diff stats
path: root/lib/pure/coro.nim
blob: 8fa529474a1343b0d02fd4e3d653191079e89f2c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#
#
#            Nim's Runtime Library
#        (c) Copyright 2015 Rokas Kupstys
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

when not defined(nimCoroutines):
  {.error: "Coroutines require -d:nimCoroutines".}

import os, times
import macros
import arch
import lists

const defaultStackSize = 512 * 1024

type Coroutine = ref object
  # prev: ptr Coroutine
  # next: ptr Coroutine
  ctx: JmpBuf
  fn: proc()
  started: bool
  lastRun: float
  sleepTime: float
  stack: pointer
  stacksize: int

var coroutines = initDoublyLinkedList[Coroutine]()
var current: Coroutine
var mainCtx: JmpBuf


proc GC_addStack(starts: pointer) {.cdecl, importc.}
proc GC_removeStack(starts: pointer) {.cdecl, importc.}
proc GC_setCurrentStack(starts, pos: pointer) {.cdecl, importc.}

proc start*(c: proc(), stacksize: int=defaultStackSize) =
  ## Adds coroutine to event loop. It does not run immediately.
  var coro = Coroutine()
  coro.fn = c
  while coro.stack == nil:
    coro.stack = alloc0(stacksize)
  coro.stacksize = stacksize
  coroutines.append(coro)

{.push stackTrace: off.}
proc suspend*(sleepTime: float=0) =
  ## Stops coroutine execution and resumes no sooner than after ``sleeptime`` seconds.
  ## Until then other coroutines are executed.
  ##
  ## This is similar to a `yield`:idx:, or a `yieldFrom`:idx in Python.
  var oldFrame = getFrame()
  var sp {.volatile.}: pointer
  GC_setCurrentStack(current.stack, cast[pointer](addr sp))
  current.sleepTime = sleepTime
  current.lastRun = epochTime()
  if setjmp(current.ctx) == 0:
    longjmp(mainCtx, 1)
  setFrame(oldFrame)
{.pop.}

proc run*() =
  ## Starts main event loop which exits when all coroutines exit. Calling this proc
  ## starts execution of first coroutine.
  var node = coroutines.head
  var minDelay: float = 0
  var frame: PFrame
  while node != nil:
    var coro = node.value
    current = coro
    os.sleep(int(minDelay * 1000))

    var remaining = coro.sleepTime - (epochTime() - coro.lastRun);
    if remaining <= 0:
      remaining = 0
      let res = setjmp(mainCtx)
      if res == 0:
        frame = getFrame()
        if coro.started:            # coroutine resumes
          longjmp(coro.ctx, 1)
        else:
          coro.started = true       # coroutine starts
          var stackEnd = cast[pointer](cast[ByteAddress](coro.stack) + coro.stacksize)
          GC_addStack(coro.stack)
          coroSwitchStack(stackEnd)
          coro.fn()
          coroRestoreStack()
          GC_removeStack(coro.stack)
          var next = node.prev
          coroutines.remove(node)
          dealloc(coro.stack)
          node = next
          setFrame(frame)
      else:
        setFrame(frame)

    elif remaining > 0:
      if minDelay > 0 and remaining > 0:
        minDelay = min(remaining, minDelay)
      else:
        minDelay = remaining

    if node == nil or node.next == nil:
      node = coroutines.head
    else:
      node = node.next

proc alive*(c: proc()): bool =
  ## Returns ``true`` if coroutine has not returned, ``false`` otherwise.
  for coro in items(coroutines):
    if coro.fn == c:
      return true

proc wait*(c: proc(), interval=0.01) =
  ## Returns only after coroutine ``c`` has returned. ``interval`` is time in seconds how often.
  while alive(c):
    suspend interval

when isMainModule:
  var stackCheckValue = 1100220033
  proc c2()

  proc c1() =
    for i in 0 .. 3:
      echo "c1"
      suspend 0.05
    echo "c1 exits"


  proc c2() =
    for i in 0 .. 3:
      echo "c2"
      suspend 0.025
    wait(c1)
    echo "c2 exits"

  start(c1)
  start(c2)
  run()
  echo "done ", stackCheckValue