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 all commits
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
8 changes: 8 additions & 0 deletions compiler/ast/ast_query.nim
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,14 @@ proc isSinkParam*(s: PSym): bool {.inline.} =
proc isSinkType*(t: PType): bool {.inline.} =
t.kind == tySink

proc isCoroutineConstr*(prc: PSym): bool {.inline.} =
## Returns whether `prc` is the symbol of a coroutine *constructor*.
sfCoroutine in prc.flags and tfCoroutine in prc.typ.flags

proc isCoroutine*(prc: PSym): bool {.inline.} =
## Returns whether `prc` is the symbol of a coroutine.
sfCoroutine in prc.flags and tfCoroutine notin prc.typ.flags

const magicsThatCanRaise* = {
mNone, mParseExprToAst, mParseStmtToAst, mEcho, mChckRange }

Expand Down
3 changes: 3 additions & 0 deletions compiler/ast/ast_types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ type
sfUsedInFinallyOrExcept ## symbol is used inside an 'except' or 'finally'
sfNoalias ## 'noalias' annotation, means C's 'restrict'
sfEffectsDelayed ## an 'effectsDelayed' parameter
sfCoroutine ## the routine is a coroutine

TSymFlags* = set[TSymFlag]

Expand Down Expand Up @@ -640,6 +641,7 @@ type
tfByCopy, ## pass object/tuple by copy (C backend)
tfByRef, ## pass object/tuple by reference (C backend)
tfIterator, ## type is really an iterator, not a tyProc
tfCoroutine, ## type is that of a coroutine
tfNotNil, ## type cannot be 'nil'
tfRequiresInit, ## type constains a "not nil" constraint somewhere or
## a `requiresInit` field, so the default zero init
Expand Down Expand Up @@ -799,6 +801,7 @@ type
mIsMainModule, mCompileDate, mCompileTime, mProcCall,
mCpuEndian, mHostOS, mHostCPU, mBuildOS, mBuildCPU, mAppType,
mCompileOption, mCompileOptionArg,
mSuspend,
mNLen, mNChild, mNSetChild, mNAdd, mNAddMultiple, mNDel,
mNKind, mNSymKind,

Expand Down
10 changes: 10 additions & 0 deletions compiler/ast/trees.nim
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,16 @@ proc effectSpec*(n: PNode, effectType: TSpecialWord): PNode =
result.add(it[1])
return

proc coroutineSpec*(n: PNode): PType =
## Returns the instance type specified by the ``.coroutine`` pragma in
## pragma list `n`, or nil, if no instance type is specified.
let p = findPragma(n, wCoroutine)
assert p != nil, "has no coroutine specification"
if p.kind == nkExprColonExpr:
p[1].typ
else:
nil

proc unnestStmts(n, result: PNode) =
case n.kind
of nkStmtList:
Expand Down
16 changes: 16 additions & 0 deletions compiler/ast/types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,11 @@ proc getProcConvMismatch*(
result[0].incl pcmNotIterator
result[1] = isNone

if (tfCoroutine in f.flags) != (tfCoroutine in a.flags):
# TODO: use the a dedicated enum field
result[0].incl pcmNotIterator
result[1] = isNone

if f.callConv != a.callConv:
# valid to pass a 'nimcall' thingie to 'closure':
if f.callConv == ccClosure and a.callConv == ccNimCall:
Expand Down Expand Up @@ -1544,3 +1549,14 @@ proc classifyBackendView*(t: PType): BackendViewKind =
tyGenericParam, tyForward, tyBuiltInTypeClass, tyCompositeTypeClass,
tyAnd, tyOr, tyNot, tyAnything, tyFromExpr:
unreachable()

proc lookupInType*(typ: PType, field: PIdent): PSym =
## Searches for a field with the given identifier (`field`) in the object
## type hierarchy of `typ`.
var typ = typ
while typ != nil:
typ = typ.skipTypes(skipPtrs)
result = lookupInRecord(typ.n, field)
if result != nil:
return
typ = typ.base
3 changes: 3 additions & 0 deletions compiler/ast/typesrenderer.nim
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,9 @@ proc typeToString*(typ: PType, prefer: TPreferedDesc = preferName): string =
result.add(')')
if t.len > 0 and t[0] != nil: result.add(": " & typeToString(t[0]))
var prag = if t.callConv == ccNimCall and tfExplicitCallConv notin t.flags: "" else: $t.callConv
if tfCoroutine in t.flags:
addSep(prag)
prag.add "coroutine"
if tfNoSideEffect in t.flags:
addSep(prag)
prag.add("noSideEffect")
Expand Down
2 changes: 1 addition & 1 deletion compiler/ast/wordrecg.nim
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ type
wStdIn = "stdin", wStdOut = "stdout", wStdErr = "stderr",

wInOut = "inout", wByCopy = "bycopy", wByRef = "byref", wOneWay = "oneway",
wBitsize = "bitsize", wImportHidden = "all",
wBitsize = "bitsize", wImportHidden = "all", wCoroutine = "coroutine"

TSpecialWords* = set[TSpecialWord]

Expand Down
1 change: 1 addition & 0 deletions compiler/front/condsyms.nim
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ proc initDefines*(symbols: StringTableRef) =
defineSymbol("nimskullNewExceptionRt")
defineSymbol("nimskullNoNkStmtListTypeAndNkBlockType")
defineSymbol("nimskullNoNkNone")
defineSymbol("nimskullHasCoroutines")
195 changes: 195 additions & 0 deletions compiler/sem/coroutines.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
## Implements the coroutine-related transformations. Despite being a separate
## module, the coroutine transformation(s) is heavily intertwined with
## `transf <#transf>`_.

import
compiler/ast/[
ast,
idents,
types
],
compiler/modules/[
magicsys,
modulegraphs
],
compiler/sem/[
closureiters,
lambdalifting,
lowerings
],
compiler/utils/[
idioms,
]

proc computeFieldStart(t: PType, pos: var int) =
## Recursively traverses `t` and updates `pos` to the field position
## of the first field a type inheriting from `t` would have.
proc aux(n: PNode, p: var int) =
case n.kind
of nkRecCase:
inc p # discriminator
for i in 1..<n.len:
aux(n[i], p)
of nkRecList:
for it in n.items:
aux(it, p)
of nkSym:
inc p
else:
unreachable()

let t = t.skipTypes(skipPtrs)
assert t.kind == tyObject
if t.len > 0 and t.base != nil:
computeFieldStart(t.base, pos)

aux(t.n, pos)

proc preTransformConstr*(g: ModuleGraph, idgen: IdGenerator, prc: PSym, body: PNode): PNode =
## Transforms the `body` of coroutine constructor `prc` into:
##
## .. code-block:: nim
##
## discard (proc inner() {.closure.} =
## body
## )
##
## Since the owner of the locals within `body` is not changed, the lambda-
## lifting pass will lift them into the environment type.
let cache = g.cache
let base = g.getCompilerProc("CoroutineBase").typ

# setup the actual coroutine:
let inner = newSym(skProc, prc.name, nextSymId idgen, prc, prc.info,
prc.options)
inner.flags.incl sfCoroutine
inner.ast = newProcNode(nkLambda, prc.info, body,
newTree(nkFormalParams, newNodeIT(nkType, prc.info, base)),
newSymNode(inner),
newNode(nkEmpty),
newNode(nkEmpty),
newNode(nkEmpty),
newNode(nkEmpty))
inner.typ = newProcType(prc.info, nextTypeId idgen, inner)
inner.typ[0] = base # return type
# temporarily mark the procedure as a closure procedure, so that the lambda-
# lifting pass visits it
inner.typ.callConv = ccClosure

# temporarily stash the ``self`` symbol node in the inner procedure's
# dispatcher slot
inner.ast.sons.setLen(dispatcherPos + 1)
inner.ast[dispatcherPos] = move prc.ast[dispatcherPos]

# result symbol for the coroutine:
inner.ast[resultPos] = newSymNode:
newSym(skResult, cache.getIdent("result"), nextSymId idgen, inner,
inner.info, base)

# place the instance base type in the constructor's dispatcher
# slot, for the lambda-lifting pass to later fetch it
prc.ast[dispatcherPos] = newNodeIT(nkType, prc.info, prc.typ[0])

# fix the result variable for the constructor:
prc.ast[resultPos] = newSymNode:
newSym(skResult, cache.getIdent("result"), nextSymId idgen, prc,
prc.info, prc.typ[0])

let body = copyNodeWithKids(inner.ast)
body.typ = inner.typ
result = nkDiscardStmt.newTree(body)

proc transformCoroutineConstr*(g: ModuleGraph, idgen: IdGenerator, prc: PSym,
body: PNode): PNode =
## Post-processes the transformed coroutine instance constructor procedure,
## completing the body of the constructor.
##
## The `body` is expected to have undergone the transf pass, including
## lambda-lifting.
let
cache = g.cache
base = g.getCompilerProc("CoroutineBase").typ
# this is a bit brittle. We rely on the exact positions in the AST
inner = body.lastSon[0][0].sym
selfSym = move inner.ast[dispatcherPos]
res = prc.ast[resultPos].sym

result = body

# remove the discard statement injected earlier:
result.delSon(result.len - 1)

let
envLocal = body[0][0][0] # the local injected by lambda-lifting
constr = body[0][0][2] # the env construction expression

# patch the environment construction:
constr.add nkExprColonExpr.newTree(
newSymNode lookupInType(base, cache.getIdent("fn")),
newSymNode inner
)
# the state needs to be initialized to -4, to signal that the instance is
# suspended:
constr.add nkExprColonExpr.newTree(
newSymNode lookupInType(base, cache.getIdent("state")),
newIntTypeNode(-4, g.getSysType(prc.info, tyInt32))
)

# add the result assignment:
result.add newAsgnStmt(newSymNode(res),
newTreeIT(nkObjDownConv, prc.info, res.typ, envLocal))
# init the lifted ``self`` symbol:
if getFieldFromObj(envLocal.typ.base, selfSym.sym) != nil:
result.add newAsgnStmt(indirectAccess(envLocal, selfSym.sym, selfSym.info),
newSymNode res)

proc transformCoroutine*(g: ModuleGraph, idgen: IdGenerator, prc: PSym,
body: PNode): PNode =
## Given the ``trans``formed and lambda-lifted `body`, applies the
## transformation for turning the coroutine `prc` into a resumable procedure
## (uses `closureiters <#closureiters>`_ underneath).
##
## Also takes care of fixing the signature of `prc`.
let
base = g.getCompilerProc("CoroutineBase").typ
body = transformClosureIterator(g, idgen, prc, body)
param = prc.getEnvParam()

# replace the hidden parameter with one that has the correct type:
let newParam = copySym(param, nextSymId idgen)
newParam.typ = base

# the original parameter is turned into a cursor local and is injected
# into the body. Reusing the symbol means that body doesn't need
# to be patched
param.kind = skLet
param.flags.incl sfCursor

# fix the signature. It needs to be ``CoroutineBase -> CoroutineBase``
prc.typ.callConv = ccNimCall
prc.typ.rawAddSon(base) # first parameter type
prc.typ.n.add newSymNode(newParam)
# replace the hidden parameter (it's no longer hidden):
prc.ast[paramsPos][^1] = newSymNode(newParam)

# inject a definition for the local and emit ``result`` initialization
result = nkStmtList.newTree(
nkLetSection.newTree(
newIdentDefs(newSymNode(param),
newTreeIT(nkObjDownConv, body.info, param.typ,
newSymNode(newParam)))),
# XXX: always assigning the continuation to the result is inefficient; it
# should happen on return
newAsgnStmt(prc.ast[resultPos],
indirectAccess(newSymNode(newParam), "next", newParam.info,
g.cache)),
body
)

# the fields in the constructed environment are wrong, they need to be
# patched
let obj = param.typ.base
var start = 0
computeFieldStart(obj.base, start)
for it in obj.n.items:
it.sym.position += start
32 changes: 27 additions & 5 deletions compiler/sem/lambdalifting.nim
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,10 @@ proc createStateField(g: ModuleGraph; iter: PSym; idgen: IdGenerator): PSym =
result.typ = createClosureIterStateType(g, iter, idgen)

proc createEnvObj(g: ModuleGraph; idgen: IdGenerator; owner: PSym; info: TLineInfo): PType =
result = createObj(g, idgen, owner, info, final=false)
if sfCoroutine in owner.flags:
result = createObj(g, idgen, owner, info, owner.ast[dispatcherPos].typ)
else:
result = createObj(g, idgen, owner, info, g.getCompilerProc("RootObj").typ)

proc getClosureIterResult*(g: ModuleGraph; iter: PSym; idgen: IdGenerator): PSym =
if resultPos < iter.ast.len:
Expand Down Expand Up @@ -591,7 +594,12 @@ proc accessEnv(available: PSym, wanted: PType, info: TLineInfo,
result = rawIndirectAccess(result, upField, info)

proc getStateField*(g: ModuleGraph; owner: PSym): PSym =
getHiddenParam(g, owner).typ.base.n[0].sym
if sfCoroutine in owner.flags:
# for coroutines, the ``state`` field symbol comes from the
# ``CoroutineBase`` type
g.getCompilerProc("CoroutineBase").typ.base.n[1].sym
else:
getHiddenParam(g, owner).typ.base.n[0].sym

proc symToClosure(n: PNode; graph: ModuleGraph; idgen: IdGenerator;
c: LiftingPass): PNode =
Expand Down Expand Up @@ -682,9 +690,10 @@ proc liftCapturedVars(n: PNode, graph: ModuleGraph, idgen: IdGenerator,
n[1] = liftCapturedVars(n[1], graph, idgen, c)
if n[1].kind == nkClosure: result = n[1]
of nkReturnStmt:
if n[0].kind in {nkAsgn, nkFastAsgn}:
if n[0].kind in {nkAsgn, nkFastAsgn} and sfCoroutine notin c.owner.flags:
# let's not touch the LHS in order to make the lifting pass
# correct when `result` is lifted
# correct when `result` is lifted. For coroutines, the LHS needs to
# be rewritten too!
n[0][1] = liftCapturedVars(n[0][1], graph, idgen, c)
else:
n[0] = liftCapturedVars(n[0], graph, idgen, c)
Expand Down Expand Up @@ -775,6 +784,19 @@ proc liftLambdas*(g: ModuleGraph; fn: PSym, body: PNode;
# callsite
result.body = liftCapturedVars(body, g, idgen, initLiftingPass(d, param))
result.env = param
elif fn.isCoroutine and fn.typ.callConv == ccClosure:
# coroutines are somewhat special. In the context of lambda-lifting,
# they're similar to closure iterators, with the difference that the
# environment parameter type is already correct
var d = initDetectionPass(g, fn, idgen)
detectCapturedVars(body, fn, d)

let param = getEnvParam(fn)
assert param != nil

prepareInnerRoutines(d, idgen, param.typ, fn.info)
result.body = liftCapturedVars(body, g, idgen, initLiftingPass(d, param))
result.env = param
elif body.kind != nkEmpty:
assert fn.typ.callConv != ccClosure or getEnvParam(fn) != nil,
"missing environment parameter"
Expand Down Expand Up @@ -895,7 +917,7 @@ proc liftForLoop*(g: ModuleGraph; body: PNode; idgen: IdGenerator;
callee = newSym(skLet, op[0].sym.name, nextSymId(idgen), owner, op.info)
callee.typ = op.typ

if owner.isIterator:
if owner.isIterator or owner.isCoroutine:
# meh, we have to add the local to the environment; it might be used
# across yields
op = freshVarForClosureIter(g, callee, idgen, owner)
Expand Down
Loading