Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce coroutines to the language #1249

Draft
wants to merge 45 commits into
base: devel
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
6eb6499
first draft of coroutine specification
zerbina Mar 18, 2024
9d5714b
spec: rename two tests
zerbina Mar 20, 2024
52eab37
spec: specify what type a coroutine has
zerbina Mar 20, 2024
695b61a
spec: define coroutine values
zerbina Mar 20, 2024
30c60a4
basic compiler/system integration for coroutines
zerbina Mar 20, 2024
03074f0
coroutines: fix `status`
zerbina Mar 21, 2024
0cde3f2
coroutines: remove `running` field
zerbina Mar 21, 2024
9c5eae1
spec: remove automatic trampolining
zerbina Mar 21, 2024
40b6fca
compiler: remove `launch` magic
zerbina Mar 21, 2024
dc9597a
coroutines: fix `resume`'s "suspendend" detection
zerbina Mar 21, 2024
b74c5ce
spec: remove the type requirement restriction from `suspend`
zerbina Mar 24, 2024
74f3796
spec: small wording improvement
zerbina Mar 24, 2024
d697a56
spec: fix the tests
zerbina Mar 26, 2024
87bd33b
spec: fix `t11_suspend_coroutine`
zerbina Mar 26, 2024
26558ad
spec: fix `t22_coroutine_value_usage`
zerbina Mar 26, 2024
73f8da0
spec: add test for forward declaration
zerbina Mar 26, 2024
f9e4fa9
spec: extend the specification
zerbina Mar 26, 2024
5220081
pragmas: basic `.coroutine` pragma support
zerbina Mar 26, 2024
5bb597f
semstmts: coroutine header validation and result handling
zerbina Mar 26, 2024
24fd461
typesrenderer: handle coroutine types
zerbina Mar 26, 2024
e46ddb9
types: coroutine-ness contributes the type equality
zerbina Mar 26, 2024
5db1412
lowerings: support providing base type to `createObj`
zerbina Mar 26, 2024
7a4f68e
implement the coroutine transformation
zerbina Mar 26, 2024
1d616b1
lambdalifting: use existing `state` field for coroutines
zerbina Mar 26, 2024
699fa4d
transf: lower `suspend`
zerbina Mar 26, 2024
1d8e01c
lowerings: re-use `result` field from environment
zerbina Mar 26, 2024
664403f
support coroutine type definitions
zerbina Mar 26, 2024
17bdf9c
semstmts: fix anonymous coroutines
zerbina Mar 26, 2024
0b0b263
fix `Coroutine.exc` type
zerbina Mar 26, 2024
1fe7ebd
basic cancellation support
zerbina Mar 26, 2024
2f418fc
implement the hidden `self` parameter
zerbina Mar 26, 2024
f31c36c
coroutines: export `CoroutineBase`
zerbina Mar 26, 2024
e42cd0d
spec: fix a typo
zerbina Mar 26, 2024
d473a74
tests: add two tests for coroutine bugs
zerbina Mar 26, 2024
d60a95e
coroutines: remove duplicate `result` symbol creation
zerbina Mar 26, 2024
759a179
coroutines: fix field patching
zerbina Mar 26, 2024
960cfc3
rework coroutine transformation
zerbina Mar 26, 2024
c2cff91
fix iterator inlining in the context of coroutines
zerbina Mar 26, 2024
ffd4136
fix run-time crash when using the VM backend
zerbina Mar 26, 2024
55008dc
spec: using `suspend` outside of coroutines is disallowed
zerbina Mar 26, 2024
2927b8d
spec: support `suspend` without parameter
zerbina Mar 26, 2024
fd712a8
implement support for parameter-less `suspend`
zerbina Mar 26, 2024
89265d5
fix compiler crash when there's an error in the body
zerbina Mar 26, 2024
86018bb
spec: support tail calls for coroutines
zerbina Apr 11, 2024
29b642e
implement tail call support for coroutines
zerbina Apr 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions tests/lang/s02_core/s99_coroutines/t01_coroutine_definition.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
discard """
"""

## Normal `proc`s and `func`s become coroutines by applying the `.coroutine`
## pragma to their definition.

proc coro1() {.coroutine.} =
discard

func coro2() {.coroutine.} =
discard
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
discard """
"""

## Except for `openArray` and `var` types, there are no restrictions on the
Copy link
Collaborator

@saem saem Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the var parameter restriction because we don't know how to handle them in the environment? Because technically we can just treat it as a ptr to the that location (out parameter), although perhaps we're inheriting this limitation from closure iterators (haven't poked at those much).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disallowing var is not so much a limitation with closure iterators, rather it's for safety. Capturing a var as a ptr is indeed possible, but then it's very easy to create a situation of dangling pointers. Consider:

proc coro(x: var int) {.coroutine.} =
  x = 1
  echo x

proc test(): Coroutine[void] =
  var y = 1
  result = launch coro(y)

# upon returning from `test`, the `y` local is gone, so reading/writing from/to
# `x` would be a use-after-(stack)-free  
resume(test())

Callsite escape analysis doesn't help, since the coroutine could save the instance in some global, for example.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, so we couldn't require var T params be treated as lent T?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to refs shared-owner ship semantics, and Coroutine beings ref, it would not work, yeah.

If Coroutine were a unique-ownership type (e.g., something akin to C++'s std::unique_ptr), then storing the var T params as lent T could work, but the borrow checker would need to understand such unique-ownership type.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, even if a coroutine callable was proc coro(self: sink Coroutine): Coroutine[T], sink doesn't work because it might be consumed (not strong enough) and that doesn't play well with the fact that we need to return it. I guess we'd have to have {.noalias.} become first class, as opposed to restrict in codgen.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's a good idea, but we could introduce a CoroutinePtr, which is a heap-allocated object that has value semantics. Making this type as ergonomic to use as a ref type would be quite tricky, however.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I don't think it'd be a good idea. If anything that capability should develop separately (guided by a few more use cases) and if successful used here. Otherwise, I'm guessing there are better solutions.

## number or type of the parameters, compared to non-coroutine procedures.

proc a(x: int, y: seq[float], z: string) {.coroutine.} =
discard

## Coroutines cannot return views, but there are no other restrictions.

proc b(): float {.coroutine.} =
discard
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
discard """
matrix: "--errorMax:2"
action: reject
"""

proc a(x: openArray[int]) {.coroutine.} = #[tt.Error
^ invalid type: 'openArray[int]']#
discard

proc b(x: var int) {.coroutine.} = #[tt.Error
^ invalid type: 'var int']#
discard
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
discard """
description: "Converters cannot be coroutines"
action: reject
"""

# XXX: maybe to restrictive, there's nothing preventing converters from being
zerbina marked this conversation as resolved.
Show resolved Hide resolved
# coroutines, even though there's likely little use of them being one

converter conv(x: int): float {.coroutine.} =
discard
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
discard """
description: "Macros cannot be coroutines"
action: reject
"""

# XXX: perhaps too restrictive; it's not impossible to implement

macro m() {.coroutine.} =
zerbina marked this conversation as resolved.
Show resolved Hide resolved
discard
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
discard """
description: "Methods cannot be coroutines"
action: reject
"""

type
Base = object of RootObj
Sub = object of RootObj

method m(x: ref Object) {.coroutine.} =
discard
27 changes: 27 additions & 0 deletions tests/lang/s02_core/s99_coroutines/t04_launch_coroutine.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

## To launch a coroutine, the built-in ``launch`` routine is used. As its
## single parameter, it expects the invocation of a coroutine.
##
## What happens is, in the following order:
## 1. a managed heap cell of the coroutine's internal environment type is
## allocated
## 2. the provided arguments are captured in the internal environment, either
## by copying or moving, depending on the parameter
## 3. the status of the couroutine instance is set to the "suspended" state
##
## The instantiated coroutine is returned as the built-in ``Coroutine[T]``
## type (where `T` is the return type of the coroutine), which is a
## polymorphic ``ref`` type. A coroutine's internal environment type is always
## derived from the ``Coroutine[T]`` type.
##
## The body of the coroutine is not executed yet.

proc coro(x: int) {.coroutine.} =
discard

# every routine invocation syntax is valid for ``launch``
let instance = launch coro(1)
zerbina marked this conversation as resolved.
Show resolved Hide resolved
doAssert instance is Coroutine[void]
doAssert instance.state == csSuspended

## It is legal to do nothing with an instantiated coroutine.
22 changes: 22 additions & 0 deletions tests/lang/s02_core/s99_coroutines/t05_resume_coroutine.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
discard """
output: "here"
"""

## To resume a coroutine instance (that is, run it until the next suspension
## point is reached), the built-in nullary ``resume`` routine is used. The
## ``resume`` procedure returns a coroutine instance, which, by default, is
## the same one that was resumed. Only an instance that is in the "suspended"
## state can be resumed.
##
## Before passing control to the coroutine, ``resume`` sets the instance's
## status to "running"

proc coro() {.coroutine.}
echo "here"

let instance = launch coro()
discard resume(instance) # the echo will be executed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it mean that an apparently immutable coroutine can change state?
Why do we need a launch operator?
Why can't we simply call instance() to resume the coroutine and recover the argument of a yield?
What purpose does returning the coroutine from resume serve?

Copy link
Collaborator

@saem saem Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What purpose does returning the coroutine from resume serve?

The point of returning a coroutine is to allow one coroutine call to "forever" yield control to another coroutine in its stead. This allows two way communication between coroutines, where it's not so much caller-callee, but "symmetric".

I recommend the wikipedia article on coroutines, the "Definition and Types" section is really good for mapping the design space (stackless/full, a/symmetric, and first-class vs constrained), and then the comparison section with subroutines illustrates the peering/symmetric relationship between a producer consumer pair.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it mean that an apparently immutable coroutine can change state?

I'm not sure I understand the question. A couroutine instance is not immutable, or at least I don't consider them to be.

Why do we need a launch operator?

To construct/create an instance of a coroutine without blocking use of the coroutine(...) standalone invocation syntax for other purposes. At present, coroutine(...) is expanded to trampoline(launch coroutine()).

Why can't we simply call instance() to resume the coroutine and recover the argument of a yield?

Having an explicit resume relies on less less-tested compiler features, but I agree that having a proc ()[T: Coroutine](c: T): T that's an alias for resume would make sense.


## When a suspension point is reached, ``resume`` returns. If there's no more
## code to run within the coroutine, prior to returning from ``resume``, the
## state is set to "pending", otherwise it's set to "suspendend".
28 changes: 28 additions & 0 deletions tests/lang/s02_core/s99_coroutines/t06_cancel_coroutine.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

## A coroutine can be cancelled, by raising an exception and letting it
## propagate to the edge of the coroutine. Upon cancellation, the
## ``resume`` call that started execution changes the state of the instance
## to "aborted" and returns the instance.

proc coro(cancel: bool) {.coroutine.}
if cancel:
raise CatchableError.newException("cancel")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zerbina did you consider/think of an alternative approach to cancellation, I'm trying to sort out how we might do:

  1. coroutine: signals error
  2. caller: recovers and resume
  3. coroutine: resumes post-recovery

I imagine that'd be pretty key for enabling something like effect handling.

It could also be that raise is a coroutine entirely giving up (irrecoverable) vs "something else" where a coroutine needs input/something to continue or it's cancelled ('graceful' cancellation). This distinction might be entirely off base.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you consider/think of an alternative approach to cancellation

I did consider an explicit cancel, but the way I imagined it seemed redundant with raising an exception, so I left it out.

So, with the current specification, one can do two things:

  • raise, and not handle, an exception from within the coroutine (terminal; coroutine instance cannot be resumed afterwards)
  • suspend and expect the caller to perform some action (like changing some coroutine instance state)

I think what you're saying, please correct me if I'm misunderstanding, is that coroutines should provide a built-in middle-ground, where the coroutine instance can signal that it can be resumed, but some action must be taken first; not doing so would raise a CoroutineError. How this could look like, I'm not sure yet.


Aside: whichever solution we pick, I think an escaping exception should abort the coroutine. My reasoning is that I think the programmer and compiler should be able reason about control-flow within a coroutine the same as they do everywhere else (minus the suspending, of course, but that should ideally be more or less opaque).

Put differently, I think that it should be possible to turn a normal procedure into a coroutine without having to change anything else, and the body continues to behave exactly the same.

If a coroutine can continue after a raise (or a normal call that raised), then that's no longer the above no longer holds, and whether raise is a terminator for structured control-flow becomes context and run-time dependent.

In short, I think raise should be "entirely giving up".

Copy link
Collaborator

@saem saem Mar 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, with the current specification, one can do two things:

  • raise, and not handle, an exception from within the coroutine (terminal; coroutine instance cannot be resumed afterwards)
  • suspend and expect the caller to perform some action (like changing some coroutine instance state)

I think what you're saying, please correct me if I'm misunderstanding, is that coroutines should provide a built-in middle-ground, where the coroutine instance can signal that it can be resumed, but some action must be taken first; not doing so would raise a CoroutineError. How this could look like, I'm not sure yet.

Yup, I think a middle ground makes sense, to share my motivation for having a middle ground, I want to see if we can drop the status field, ideally more, and I believe the middle ground will be required in fulfilling that.

I'll pick up dropping the state field below the section break.

Aside: whichever solution we pick, I think an escaping exception should abort the coroutine. My reasoning is that I think the programmer and compiler should be able reason about control-flow within a coroutine the same as they do everywhere else (minus the suspending, of course, but that should ideally be more or less opaque).

I also agree that raising exceptions, and their impact on control flow, should remain as expected (hah!) and therefore predictable based on existing reasoning/intuition.

Put differently, I think that it should be possible to turn a normal procedure into a coroutine without having to change anything else, and the body continues to behave exactly the same.

This is a good property, because any background CPS transform, or the reverse, should be equivalent -- otherwise we've got bigger problems. 😬

If a coroutine can continue after a raise (or a normal call that raised), then that's no longer the above no longer holds, and whether raise is a terminator for structured control-flow becomes context and run-time dependent.

In short, I think raise should be "entirely giving up".

Yup, on the same page; raise is an 'abort', instead of a 'fail/error'.


Dropping the state Field

I'd like to drop the state field, for possibly the same reasons @disruptek suggested removing it, mine are:

  • it reduces the amount of mutable state
  • the state of a coroutine is a derivation of it's internal control flow, so the field is really a cache than the reality
  • it invites the temptation of fiddling with it
  • smaller coroutine instances; moar fast!

Instead, at least at a low level, a coroutine should yields those state values (statically knowable within a transformed coroutine definition body). For completion, where completion might have a non-void return value a finishedWithResult state that informs the receiver that a 'result' field both exists and it contains a value of type T for a Coroutine[T]. This path might allow us to shrink CoroutineBase of other fields as well.

Otherwise, where state needs to be queried later, that burden can be carried by any runner, out of band, or by extended/more stateful coroutine instance types.


let instance = launch coro(true)
doAssert instance.status == csSuspended

resume(instance)
doAssert instance.status == csAborted

## If a coroutine instance is cancelled, the exception that caused
## cancellation is stored in the coroutine instance object. It can be
## extracted by using the built-in ``unwrap``.
Comment on lines +17 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this preferable to simply raising the exception in the code path where it's running?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because presently the exception is doing double duty for exceptions + cancellation. At the moment, I think cancellation needs its own thing, but that needs syntax/some operator and still jive with exceptions, as those can be raised by calls subordinate to the coroutine.

What should be possible is assuming there is an executing coroutine (coro) and it encounters an error: it should be able to signal to the caller than an error occurred, the caller should be able to recover, and then it can resume execution of coro.


# XXX: unwrap needs a better name

let error = unwrap(instance)
doAssert error of CatchableError
doAssert error.msg == "fail"

## The call to ``unwrap`` moves the instance into the "finished" state.
doAssert instance.status == csFinished
37 changes: 37 additions & 0 deletions tests/lang/s02_core/s99_coroutines/t07_finish_coroutine.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## Much like normal procedures, a coroutine can yield a result. The `result`
## variable is stored as part of the coroutine instance object. To extract,
## the result value, the built-in ``finish`` procedure needs to be called
## on a "pending" instance. An instance enters the "pending" state when
## the end of the coroutine's body is reached.

proc coro(): int {.coroutine.} =
result = 1

let instance = launch coro()
resume(instance) # run to completion
doAssert instance.status == csPending

doAssert finish(instance) == 1

## A successful call to ``finish`` moves the instance into the "finished"
## state.
doAssert instance.status == csFinished

## An instance also enters the "pending" state when the coroutine exits due to
## a ``return`` being executed.

proc coro2(early: bool): int {.coroutine.} =
if early:
return 2
return 1

var instance2 = launch coro2(true)

resume(instance2)
doAssert instance2.status == csPending

doAssert finish(instance2) == 2
doAssert instance2.status == csFinished

## "finished" is the terminal state. There's no other state that the instance
## can change to from it.
15 changes: 15 additions & 0 deletions tests/lang/s02_core/s99_coroutines/t08_finish_with_void.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

## If a coroutine has no result (i.e., the return type is ``void``), then
## ``finish`` returns nothing and only changes the status from "pending" to
## "finished".

proc coro() {.coroutine.} =
discard

var instance = launch coro()

resume(instance)
doAssert instance.status == csPending

finish(instance)
doAssert instance.status == csFinished
18 changes: 18 additions & 0 deletions tests/lang/s02_core/s99_coroutines/t09_finish_does_move.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

## Calling ``finish`` moves the result value out of the instance.

type Object = object
value: int

# copying is disallowed
proc `=copy`(x: var Object, y: Object) {.error.}

proc coro(): Object {.coroutine.} =
result = Object(value: 1)

var instance = launch coro()
resume(instance)

# the value is moved out of the instance, no copy is made
let o = finish(instance)
doAssert o.value == 1
17 changes: 17 additions & 0 deletions tests/lang/s02_core/s99_coroutines/t10_self_parameter.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
discard """
output: "Coroutine[void]\ncsRunning"
"""

## To access the active *instance* within the body of a coroutine, the hidden
## ``self`` parameter is made available.

# XXX: not a good solution, either the ``self`` parameter should be explicit
# (somehow), or there should be a magic procedure
Copy link

@blackmius blackmius Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

declaring self explicitly is better imo

proc coro(self: Coroutine) {.coroutine.} =
  echo typeof(self)
  echo self.status

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed; if we're not going to have colorless functions, we should go all the way into color. Otherwise, it's just confusing to both programmers and metaprogrammers.

Copy link
Collaborator

@saem saem Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it is, proc c(a: int) {.coroutine.} describes at least two things, one is the coroutine constructor, and the other is all the fields (parameter list) that are part of the backing closure environment. The body of c assumes both self and an unpacked a.

We could have proc c(self: Coroutine, a: int) {.coroutine.}, but besides being redundant, it only provides an optional rename of the self parameter and specification of an alternative Coroutine base type. With that said:

  1. self's type isn't Coroutine, that's actually the base type
  2. renaming self is likely not helpful for most readers (misfeature)
  3. we can do named parameter shenanigans?

If you really want to be explicit, then it'd be more like this:

proc coro(#[constructor params go here]#) {.coroutine.} =
  proc (self: Coroutine) =
    echo typeof(self)
    echo self.status
  # `coroutine` pragma makes the return type a `void proc closure/Coroutine[void]`

Copy link
Collaborator Author

@zerbina zerbina Mar 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had considered this, but I'm not sure if it's better. The first parameter having special meaning seems inconsistent with the rest of the language, where parameter position doesn't have special meaning.

I think it's also somewhat confusing for the programmer:

proc coro(self: Coroutine) {.coroutine.} =
  discard

# why is there no argument passed to the coroutine constructor?
discard launch(coro())

Edit: this boils down to what @saem said, but I missed said comment due to not refreshing the page

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're not going to have colorless functions

I'm not sure I understand this remark. The feature as currently specified doesn't introduce "color", in that the coroutines can be launched and resumed from everywhere, without tainting the caller.


proc coro() {.coroutine.} =
# the self parameter is of type ``Coroutine[void]``
echo typeof(self)
echo self.status

var instance = launch coro()
resume(instance)
48 changes: 48 additions & 0 deletions tests/lang/s02_core/s99_coroutines/t11_suspend_coroutine.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
discard """
output: '''
outside: 1
coro: 1
outside: 2
coro: 2
outside: 3
coro: 3'''
"""

## A coroutine can be suspended without returning or raising an exception.
## This is achieved by calling the built-in ``suspend`` routine, which expects
## a ``CoroutineBase`` as the parameter -- the instance passed to ``suspend``
## is what ``resume`` will return.

# IDEA: instead of the ``suspend`` routine, the ``yield`` keyword could be
# re-used, which could be much more convenient to use. It'd also be
# a bit easier to implement

proc coro() {.coroutine.} =
echo "coro: 1"
suspend(self)
zerbina marked this conversation as resolved.
Show resolved Hide resolved
echo "coro: 2"
suspend(self)
echo "coro: 3"

var instance = launch coro()
let original = instance

echo "outside: 1"
instance = resume(instance)

## After suspending, the coroutine is the "suspended" state.
doAssert instance.status == csSuspended

echo "outside: 2"
instance = resume(instance)
echo "outside: 3"
instance = resume(instance)

doAssert instance.status == csPending
doAssert instance == original

## The run-time type of the instance needs to be a sub-type of the coroutine
## instance type that is suspended from, if it isn't, a ``SuspendDefect`` is
## raised.

# TODO: a test is missing
17 changes: 17 additions & 0 deletions tests/lang/s02_core/s99_coroutines/t12_suspend_with_nil.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## It is valid to pass `nil` to a ``suspend`` call, in which case ``resume``
## will return `nil`.

proc coro() {.coroutine.} =
suspend(nil)

var instance = launch coro()
# hold on to the instance:
var original = instance

doAssert resume(instance) == nil
doAssert original.status == csSuspended

# resume the instance. On reaching the body's end, the instance itself is
# returned
doAssert resume(original) == original
doAssert original.status == csPending
19 changes: 19 additions & 0 deletions tests/lang/s02_core/s99_coroutines/t13_custom_suspend.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
discard """
output: "custom"
"""

## The ``suspend`` built-in procedure is not special-cased with regards to
## lookup. It can be overloaded like any other routine.

template suspend(c: Coroutine) =
echo "custom"
system.suspend(c)

# XXX: this could be a problem. A module could export a routine such as the
# above and thus override all suspend calls in the importing module!

proc coro() {.coroutine.} =
suspend(self)

var instance = launch coro()
resume(instance)
24 changes: 24 additions & 0 deletions tests/lang/s02_core/s99_coroutines/t14_illegal_operation.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

## What operations are valid on a coroutine instance depends on its state.
## If performing an operation on a coroutine while in a state where the
## operation is not applicable, a ``CoroutineError`` is raised. The coroutine
## instance is not modified in this case.

# XXX: a catchable error could be annoying in `.raises: []` routines where
# it's guaranteed that the operation is valid. Perhaps it should
# be a defect?

proc coro() {.coroutine.} =
discard

let instance = launch coro(false)
doAssert instance.status == csSuspended

var wasError = false
# unwrap only legal when instance was aborted
try:
unwrap(instance)
except CoroutineError as e:
wasError = true

doAssert wasError
22 changes: 22 additions & 0 deletions tests/lang/s02_core/s99_coroutines/t15_custom_coroutine_type.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
discard """
output: "0"
"""

## The ``.coroutine`` pragma can be supplied with a type, which the internal
## coroutine environment object then derives from. The type must be a non-
## final ``ref`` sub-type of ``Coroutine[T]``, where `T` is the return type
## of the coroutine.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this should evaporate with the explicit first argument defining the coroutine's type.


# TODO: a negative test is missing

type Custom = ref object of Coroutine[void]
value: int

proc coro() {.coroutine: Custom.} =
# the hidden `self` parameter is also of type ``Custom``
echo self.value

## The instance returned by ``launch`` is of the provided coroutine type.
var instance = launch coro()
doAssert typeof(instance) is Custom
doAssert instance.value == 0
12 changes: 12 additions & 0 deletions tests/lang/s02_core/s99_coroutines/t16_generic_coroutine.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
discard """
output: "1"
"""

## A coroutine can be a generic routine.

proc coro[T](x: T) {.coroutine.} =
echo x

var instance = launch coro(1)
doAssert instance is Coroutine[int]
resume(instance)
Loading