nim-chronos/chronos/asyncmacro2.nim

321 lines
11 KiB
Nim

#
#
# Nim's Runtime Library
# (c) Copyright 2015 Dominik Picheta
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
import std/[macros]
# `quote do` will ruin line numbers so we avoid it using these helpers
proc completeWithResult(fut, baseType: NimNode): NimNode {.compileTime.} =
# when `baseType` is void:
# complete(`fut`)
# else:
# complete(`fut`, result)
if baseType.eqIdent("void"):
# Shortcut if we know baseType at macro expansion time
newCall(ident "complete", fut)
else:
# `baseType` might be generic and resolve to `void`
nnkWhenStmt.newTree(
nnkElifExpr.newTree(
nnkInfix.newTree(ident "is", baseType, ident "void"),
newCall(ident "complete", fut)
),
nnkElseExpr.newTree(
newCall(ident "complete", fut, ident "result")
)
)
proc completeWithNode(fut, baseType, node: NimNode): NimNode {.compileTime.} =
# when typeof(`node`) is void:
# `node` # statement / explicit return
# -> completeWithResult(fut, baseType)
# else: # expression / implicit return
# complete(`fut`, `node`)
if node.kind == nnkEmpty: # shortcut when known at macro expanstion time
completeWithResult(fut, baseType)
else:
# Handle both expressions and statements - since the type is not know at
# macro expansion time, we delegate this choice to a later compilation stage
# with `when`.
nnkWhenStmt.newTree(
nnkElifExpr.newTree(
nnkInfix.newTree(
ident "is", nnkTypeOfExpr.newTree(node), ident "void"),
newStmtList(
node,
completeWithResult(fut, baseType)
)
),
nnkElseExpr.newTree(
newCall(ident "complete", fut, node)
)
)
proc processBody(node, fut, baseType: NimNode): NimNode {.compileTime.} =
#echo(node.treeRepr)
case node.kind
of nnkReturnStmt:
let
res = newNimNode(nnkStmtList, node)
res.add completeWithNode(fut, baseType, processBody(node[0], fut, baseType))
res.add newNimNode(nnkReturnStmt, node).add(newNilLit())
res
of RoutineNodes-{nnkTemplateDef}:
# skip all the nested procedure definitions
node
else:
for i in 0 ..< node.len:
# We must not transform nested procedures of any form, otherwise
# `fut` will be used for all nested procedures as their own
# `retFuture`.
node[i] = processBody(node[i], fut, baseType)
node
proc getName(node: NimNode): string {.compileTime.} =
case node.kind
of nnkSym:
return node.strVal
of nnkPostfix:
return node[1].strVal
of nnkIdent:
return node.strVal
of nnkEmpty:
return "anonymous"
else:
error("Unknown name.")
macro unsupported(s: static[string]): untyped =
error s
proc params2(someProc: NimNode): NimNode =
# until https://github.com/nim-lang/Nim/pull/19563 is available
if someProc.kind == nnkProcTy:
someProc[0]
else:
params(someProc)
proc cleanupOpenSymChoice(node: NimNode): NimNode {.compileTime.} =
# Replace every Call -> OpenSymChoice by a Bracket expr
# ref https://github.com/nim-lang/Nim/issues/11091
if node.kind in nnkCallKinds and
node[0].kind == nnkOpenSymChoice and node[0].eqIdent("[]"):
result = newNimNode(nnkBracketExpr)
for child in node[1..^1]:
result.add(cleanupOpenSymChoice(child))
else:
result = node.copyNimNode()
for child in node:
result.add(cleanupOpenSymChoice(child))
proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
## This macro transforms a single procedure into a closure iterator.
## The ``async`` macro supports a stmtList holding multiple async procedures.
if prc.kind notin {nnkProcTy, nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
error("Cannot transform " & $prc.kind & " into an async proc." &
" proc/method definition or lambda node expected.", prc)
let returnType = cleanupOpenSymChoice(prc.params2[0])
# Verify that the return type is a Future[T]
let baseType =
if returnType.kind == nnkEmpty:
ident "void"
elif not (
returnType.kind == nnkBracketExpr and eqIdent(returnType[0], "Future")):
error(
"Expected return type of 'Future' got '" & repr(returnType) & "'", prc)
return
else:
returnType[1]
let
baseTypeIsVoid = baseType.eqIdent("void")
futureVoidType = nnkBracketExpr.newTree(ident "Future", ident "void")
if prc.kind in {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
let
prcName = prc.name.getName
outerProcBody = newNimNode(nnkStmtList, prc.body)
# Copy comment for nimdoc
if prc.body.len > 0 and prc.body[0].kind == nnkCommentStmt:
outerProcBody.add(prc.body[0])
let
internalFutureSym = ident "chronosInternalRetFuture"
internalFutureType =
if baseTypeIsVoid: futureVoidType
else: returnType
castFutureSym = nnkCast.newTree(internalFutureType, internalFutureSym)
procBody = prc.body.processBody(castFutureSym, baseType)
# don't do anything with forward bodies (empty)
if procBody.kind != nnkEmpty:
let
# fix #13899, `defer` should not escape its original scope
procBodyBlck = nnkBlockStmt.newTree(newEmptyNode(), procBody)
resultDecl = nnkWhenStmt.newTree(
# when `baseType` is void:
nnkElifExpr.newTree(
nnkInfix.newTree(ident "is", baseType, ident "void"),
quote do:
template result: auto {.used.} =
{.fatal: "You should not reference the `result` variable inside" &
" a void async proc".}
),
# else:
nnkElseExpr.newTree(
newStmtList(
quote do: {.push warning[resultshadowed]: off.},
# var result: `baseType`
nnkVarSection.newTree(
nnkIdentDefs.newTree(ident "result", baseType, newEmptyNode())),
quote do: {.pop.},
)
)
)
completeDecl = completeWithNode(castFutureSym, baseType, procBodyBlck)
closureBody = newStmtList(resultDecl, completeDecl)
internalFutureParameter = nnkIdentDefs.newTree(
internalFutureSym, newIdentNode("FutureBase"), newEmptyNode())
iteratorNameSym = genSym(nskIterator, $prcName)
closureIterator = newProc(
iteratorNameSym,
[newIdentNode("FutureBase"), internalFutureParameter],
closureBody, nnkIteratorDef)
iteratorNameSym.copyLineInfo(prc)
closureIterator.pragma = newNimNode(nnkPragma, lineInfoFrom=prc.body)
closureIterator.addPragma(newIdentNode("closure"))
# `async` code must be gcsafe
closureIterator.addPragma(newIdentNode("gcsafe"))
# TODO when push raises is active in a module, the iterator here inherits
# that annotation - here we explicitly disable it again which goes
# against the spirit of the raises annotation - one should investigate
# here the possibility of transporting more specific error types here
# for example by casting exceptions coming out of `await`..
let raises = nnkBracket.newTree()
when chronosStrictException:
raises.add(newIdentNode("CatchableError"))
else:
raises.add(newIdentNode("Exception"))
closureIterator.addPragma(nnkExprColonExpr.newTree(
newIdentNode("raises"),
raises
))
# If proc has an explicit gcsafe pragma, we add it to iterator as well.
# TODO if these lines are not here, srcloc tests fail (!)
if prc.pragma.findChild(it.kind in {nnkSym, nnkIdent} and
it.strVal == "gcsafe") != nil:
closureIterator.addPragma(newIdentNode("gcsafe"))
outerProcBody.add(closureIterator)
# -> let resultFuture = newFuture[T]()
# declared at the end to be sure that the closure
# doesn't reference it, avoid cyclic ref (#203)
let
retFutureSym = ident "resultFuture"
retFutureSym.copyLineInfo(prc)
# Do not change this code to `quote do` version because `instantiationInfo`
# will be broken for `newFuture()` call.
outerProcBody.add(
newLetStmt(
retFutureSym,
newCall(newTree(nnkBracketExpr, ident "newFuture", baseType),
newLit(prcName))
)
)
# -> resultFuture.internalClosure = iterator
outerProcBody.add(
newAssignment(
newDotExpr(retFutureSym, newIdentNode("internalClosure")),
iteratorNameSym)
)
# -> futureContinue(resultFuture))
outerProcBody.add(
newCall(newIdentNode("futureContinue"), retFutureSym)
)
# -> return resultFuture
outerProcBody.add newNimNode(nnkReturnStmt, prc.body[^1]).add(retFutureSym)
prc.body = outerProcBody
if prc.kind notin {nnkProcTy, nnkLambda}: # TODO: Nim bug?
prc.addPragma(newColonExpr(ident "stackTrace", ident "off"))
# See **Remark 435** in this file.
# https://github.com/nim-lang/RFCs/issues/435
prc.addPragma(newIdentNode("gcsafe"))
prc.addPragma(nnkExprColonExpr.newTree(
newIdentNode("raises"),
nnkBracket.newTree()
))
if baseTypeIsVoid:
if returnType.kind == nnkEmpty:
# Add Future[void]
prc.params2[0] = futureVoidType
prc
template await*[T](f: Future[T]): untyped =
when declared(chronosInternalRetFuture):
chronosInternalRetFuture.internalChild = f
# `futureContinue` calls the iterator generated by the `async`
# transformation - `yield` gives control back to `futureContinue` which is
# responsible for resuming execution once the yielded future is finished
yield chronosInternalRetFuture.internalChild
# `child` is guaranteed to have been `finished` after the yield
if chronosInternalRetFuture.internalMustCancel:
raise newCancelledError()
# `child` released by `futureContinue`
chronosInternalRetFuture.internalChild.internalCheckComplete()
when T isnot void:
cast[type(f)](chronosInternalRetFuture.internalChild).internalRead()
else:
unsupported "await is only available within {.async.}"
template awaitne*[T](f: Future[T]): Future[T] =
when declared(chronosInternalRetFuture):
chronosInternalRetFuture.internalChild = f
yield chronosInternalRetFuture.internalChild
if chronosInternalRetFuture.internalMustCancel:
raise newCancelledError()
cast[type(f)](chronosInternalRetFuture.internalChild)
else:
unsupported "awaitne is only available within {.async.}"
macro async*(prc: untyped): untyped =
## Macro which processes async procedures into the appropriate
## iterators and yield statements.
if prc.kind == nnkStmtList:
result = newStmtList()
for oneProc in prc:
result.add asyncSingleProc(oneProc)
else:
result = asyncSingleProc(prc)
when chronosDumpAsync:
echo repr result