nim-chronos/chronos/asyncmacro2.nim

328 lines
12 KiB
Nim
Raw Normal View History

2018-05-16 08:22:34 +00:00
#
#
# 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]
2018-05-16 08:22:34 +00:00
proc skipUntilStmtList(node: NimNode): NimNode {.compileTime.} =
# Skips a nest of StmtList's.
result = node
if node[0].kind == nnkStmtList:
result = skipUntilStmtList(node[0])
proc processBody(node, retFutureSym: NimNode,
2021-12-08 11:30:12 +00:00
subTypeIsVoid: bool): NimNode {.compileTime.} =
2018-05-16 08:22:34 +00:00
#echo(node.treeRepr)
result = node
case node.kind
of nnkReturnStmt:
result = newNimNode(nnkStmtList, node)
# As I've painfully found out, the order here really DOES matter.
if node[0].kind == nnkEmpty:
if not subTypeIsVoid:
result.add newCall(newIdentNode("complete"), retFutureSym,
newIdentNode("result"))
else:
result.add newCall(newIdentNode("complete"), retFutureSym)
else:
2021-12-08 11:30:12 +00:00
let x = node[0].processBody(retFutureSym, subTypeIsVoid)
2018-05-16 08:22:34 +00:00
if x.kind == nnkYieldStmt: result.add x
else:
result.add newCall(newIdentNode("complete"), retFutureSym, x)
result.add newNimNode(nnkReturnStmt, node).add(newNilLit())
return # Don't process the children of this return stmt
of RoutineNodes-{nnkTemplateDef}:
# skip all the nested procedure definitions
return node
2018-05-16 08:22:34 +00:00
else: discard
for i in 0 ..< result.len:
# We must not transform nested procedures of any form, otherwise
# `retFutureSym` will be used for all nested procedures as their own
# `retFuture`.
2021-12-08 11:30:12 +00:00
result[i] = processBody(result[i], retFutureSym, subTypeIsVoid)
2018-05-16 08:22:34 +00:00
proc getName(node: NimNode): string {.compileTime.} =
case node.kind
of nnkSym:
return node.strVal
2018-05-16 08:22:34 +00:00
of nnkPostfix:
return node[1].strVal
of nnkIdent:
return node.strVal
of nnkEmpty:
return "anonymous"
else:
error("Unknown name.")
proc isInvalidReturnType(typeName: string): bool =
return typeName notin ["Future"] #, "FutureStream"]
proc verifyReturnType(typeName: string) {.compileTime.} =
if typeName.isInvalidReturnType:
error("Expected return type of 'Future' got '" & typeName & "'")
2018-05-16 08:22:34 +00:00
2019-08-15 16:35:42 +00:00
macro unsupported(s: static[string]): untyped =
error s
2022-03-30 13:13:58 +00:00
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))
2022-03-30 13:13:58 +00:00
else:
result = node.copyNimNode()
for child in node:
result.add(cleanupOpenSymChoice(child))
2018-05-16 08:22:34 +00:00
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." &
2018-05-16 08:22:34 +00:00
" proc/method definition or lambda node expected.")
let returnType =
cleanupOpenSymChoice(if prc.kind == nnkProcTy: prc[0][0] else: prc.params[0])
2018-05-16 08:22:34 +00:00
var baseType: NimNode
# Verify that the return type is a Future[T]
if returnType.kind == nnkBracketExpr:
let fut = repr(returnType[0])
verifyReturnType(fut)
baseType = returnType[1]
elif returnType.kind == nnkEmpty:
baseType = returnType
else:
verifyReturnType(repr(returnType))
let subtypeIsVoid = returnType.kind == nnkEmpty or
(baseType.kind == nnkIdent and returnType[1].eqIdent("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])
# -> iterator nameIter(chronosInternalRetFuture: Future[T]): FutureBase {.closure.} =
# -> {.push warning[resultshadowed]: off.}
# -> var result: T
# -> {.pop.}
# -> <proc_body>
# -> complete(chronosInternalRetFuture, result)
let
internalFutureSym = ident "chronosInternalRetFuture"
iteratorNameSym = genSym(nskIterator, $prcName)
var
procBody = prc.body.processBody(internalFutureSym, subtypeIsVoid)
# don't do anything with forward bodies (empty)
if procBody.kind != nnkEmpty:
if subtypeIsVoid:
let resultTemplate = quote do:
template result: auto {.used.} =
{.fatal: "You should not reference the `result` variable inside" &
" a void async proc".}
procBody = newStmtList(resultTemplate, procBody)
# fix #13899, `defer` should not escape its original scope
procBody = newStmtList(newTree(nnkBlockStmt, newEmptyNode(), procBody))
if not subtypeIsVoid:
procBody.insert(0, newNimNode(nnkPragma).add(newIdentNode("push"),
newNimNode(nnkExprColonExpr).add(newNimNode(nnkBracketExpr).add(
newIdentNode("warning"), newIdentNode("resultshadowed")),
newIdentNode("off")))) # -> {.push warning[resultshadowed]: off.}
procBody.insert(1, newNimNode(nnkVarSection, prc.body).add(
newIdentDefs(newIdentNode("result"), baseType))) # -> var result: T
procBody.insert(2, newNimNode(nnkPragma).add(
newIdentNode("pop"))) # -> {.pop.})
procBody.add(
newCall(newIdentNode("complete"),
internalFutureSym, newIdentNode("result"))) # -> complete(chronosInternalRetFuture, result)
else:
# -> complete(chronosInternalRetFuture)
procBody.add(newCall(newIdentNode("complete"), internalFutureSym))
let
internalFutureType =
if subtypeIsVoid:
newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(newIdentNode("void"))
else: returnType
internalFutureParameter = nnkIdentDefs.newTree(internalFutureSym, internalFutureType, newEmptyNode())
closureIterator = newProc(iteratorNameSym, [newIdentNode("FutureBase"), internalFutureParameter],
2018-05-16 08:22:34 +00:00
procBody, nnkIteratorDef)
closureIterator.pragma = newNimNode(nnkPragma, lineInfoFrom=prc.body)
closureIterator.addPragma(newIdentNode("closure"))
# **Remark 435**: We generate a proc with an inner iterator which call each other
# recursively. The current Nim compiler is not smart enough to infer
# the `gcsafe`-ty aspect of this setup, so we always annotate it explicitly
# with `gcsafe`. This means that the client code is always enforced to be
# `gcsafe`. This is still **safe**, the compiler still checks for `gcsafe`-ty
# regardless, it is only helping the compiler's inference algorithm. See
# https://github.com/nim-lang/RFCs/issues/435
# for more details.
2018-05-16 08:22:34 +00:00
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 not defined(chronosStrictException):
raises.add(newIdentNode("Exception"))
else:
raises.add(newIdentNode("CatchableError"))
when (NimMajor, NimMinor) < (1, 4):
raises.add(newIdentNode("Defect"))
closureIterator.addPragma(nnkExprColonExpr.newTree(
newIdentNode("raises"),
raises
))
# If proc has an explicit gcsafe pragma, we add it to iterator as well.
if prc.pragma.findChild(it.kind in {nnkSym, nnkIdent} and
it.strVal == "gcsafe") != nil:
closureIterator.addPragma(newIdentNode("gcsafe"))
outerProcBody.add(closureIterator)
# -> var resultFuture = newFuture[T]()
# declared at the end to be sure that the closure
# doesn't reference it, avoid cyclic ref (#203)
var retFutureSym = ident "resultFuture"
var subRetType =
if returnType.kind == nnkEmpty:
newIdentNode("void")
else:
baseType
# Do not change this code to `quote do` version because `instantiationInfo`
# will be broken for `newFuture()` call.
outerProcBody.add(
newVarStmt(
retFutureSym,
newCall(newTree(nnkBracketExpr, ident "newFuture", subRetType),
newLit(prcName))
)
)
# -> resultFuture.closure = iterator
outerProcBody.add(
newAssignment(
newDotExpr(retFutureSym, newIdentNode("closure")),
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"))
2019-08-15 16:35:42 +00:00
# See **Remark 435** in this file.
2021-11-19 00:07:46 +00:00
# https://github.com/nim-lang/RFCs/issues/435
prc.addPragma(newIdentNode("gcsafe"))
let raises = nnkBracket.newTree()
when (NimMajor, NimMinor) < (1, 4):
raises.add(newIdentNode("Defect"))
prc.addPragma(nnkExprColonExpr.newTree(
newIdentNode("raises"),
raises
))
2018-05-16 08:22:34 +00:00
if subtypeIsVoid:
# Add discardable pragma.
if returnType.kind == nnkEmpty:
# Add Future[void]
prc.params[0] =
newNimNode(nnkBracketExpr, prc)
.add(newIdentNode("Future"))
.add(newIdentNode("void"))
prc
2018-05-16 08:22:34 +00:00
#echo(treeRepr(result))
exception tracking (#166) * exception tracking This PR adds minimal exception tracking to chronos, moving the goalpost one step further. In particular, it becomes invalid to raise exceptions from `callSoon` callbacks: this is critical for writing correct error handling because there's no reasonable way that a user of chronos can possibly _reason_ about exceptions coming out of there: the event loop will be in an indeterminite state when the loop is executing an _random_ callback. As expected, there are several issues in the error handling of chronos: in particular, it will end up in an inconsistent internal state whenever the selector loop operations fail, because the internal state update functions are not written in an exception-safe way. This PR turns this into a Defect, which probably is not the optimal way of handling things - expect more work to be done here. Some API have no way of reporting back errors to callers - for example, when something fails in the accept loop, there's not much it can do, and no way to report it back to the user of the API - this has been fixed with the new accept flow - the old one should be deprecated. Finally, there is information loss in the API: in composite operations like `poll` and `waitFor` there's no way to differentiate internal errors from user-level errors originating from callbacks. * store `CatchableError` in future * annotate proc's with correct raises information * `selectors2` to avoid non-CatchableError IOSelectorsException * `$` should never raise * remove unnecessary gcsafe annotations * fix exceptions leaking out of timer waits * fix some imports * functions must signal raising the union of all exceptions across all platforms to enable cross-platform code * switch to unittest2 * add `selectors2` which supercedes the std library version and fixes several exception handling issues in there * fixes * docs, platform-independent eh specifiers for some functions * add feature flag for strict exception mode also bump version to 3.0.0 - _most_ existing code should be compatible with this version of exception handling but some things might need fixing - callbacks, existing raises specifications etc. * fix AsyncCheck for non-void T
2021-03-24 09:08:33 +00:00
template await*[T](f: Future[T]): untyped =
2019-08-15 13:32:46 +00:00
when declared(chronosInternalRetFuture):
#work around https://github.com/nim-lang/Nim/issues/19193
2019-08-15 16:35:42 +00:00
when not declaredInScope(chronosInternalTmpFuture):
var chronosInternalTmpFuture {.inject.}: FutureBase = f
else:
chronosInternalTmpFuture = f
2019-08-15 13:32:46 +00:00
chronosInternalRetFuture.child = chronosInternalTmpFuture
# This "yield" is meant for a closure iterator in the caller.
2019-08-15 13:32:46 +00:00
yield chronosInternalTmpFuture
# By the time we get control back here, we're guaranteed that the Future we
# just yielded has been completed (success, failure or cancellation),
# through a very complicated mechanism in which the caller proc (a regular
# closure) adds itself as a callback to chronosInternalTmpFuture.
#
# Callbacks are called only after completion and a copy of the closure
# iterator that calls this template is still in that callback's closure
# environment. That's where control actually gets back to us.
2019-08-15 13:32:46 +00:00
chronosInternalRetFuture.child = nil
if chronosInternalRetFuture.mustCancel:
raise newCancelledError()
chronosInternalTmpFuture.internalCheckComplete()
exception tracking (#166) * exception tracking This PR adds minimal exception tracking to chronos, moving the goalpost one step further. In particular, it becomes invalid to raise exceptions from `callSoon` callbacks: this is critical for writing correct error handling because there's no reasonable way that a user of chronos can possibly _reason_ about exceptions coming out of there: the event loop will be in an indeterminite state when the loop is executing an _random_ callback. As expected, there are several issues in the error handling of chronos: in particular, it will end up in an inconsistent internal state whenever the selector loop operations fail, because the internal state update functions are not written in an exception-safe way. This PR turns this into a Defect, which probably is not the optimal way of handling things - expect more work to be done here. Some API have no way of reporting back errors to callers - for example, when something fails in the accept loop, there's not much it can do, and no way to report it back to the user of the API - this has been fixed with the new accept flow - the old one should be deprecated. Finally, there is information loss in the API: in composite operations like `poll` and `waitFor` there's no way to differentiate internal errors from user-level errors originating from callbacks. * store `CatchableError` in future * annotate proc's with correct raises information * `selectors2` to avoid non-CatchableError IOSelectorsException * `$` should never raise * remove unnecessary gcsafe annotations * fix exceptions leaking out of timer waits * fix some imports * functions must signal raising the union of all exceptions across all platforms to enable cross-platform code * switch to unittest2 * add `selectors2` which supercedes the std library version and fixes several exception handling issues in there * fixes * docs, platform-independent eh specifiers for some functions * add feature flag for strict exception mode also bump version to 3.0.0 - _most_ existing code should be compatible with this version of exception handling but some things might need fixing - callbacks, existing raises specifications etc. * fix AsyncCheck for non-void T
2021-03-24 09:08:33 +00:00
when T isnot void:
cast[type(f)](chronosInternalTmpFuture).internalRead()
2019-08-15 13:32:46 +00:00
else:
2019-08-15 16:35:42 +00:00
unsupported "await is only available within {.async.}"
2019-08-15 13:32:46 +00:00
template awaitne*[T](f: Future[T]): Future[T] =
when declared(chronosInternalRetFuture):
#work around https://github.com/nim-lang/Nim/issues/19193
2019-08-15 16:35:42 +00:00
when not declaredInScope(chronosInternalTmpFuture):
var chronosInternalTmpFuture {.inject.}: FutureBase = f
else:
chronosInternalTmpFuture = f
2019-08-15 13:32:46 +00:00
chronosInternalRetFuture.child = chronosInternalTmpFuture
yield chronosInternalTmpFuture
chronosInternalRetFuture.child = nil
if chronosInternalRetFuture.mustCancel:
raise newCancelledError()
2019-08-15 13:32:46 +00:00
cast[type(f)](chronosInternalTmpFuture)
else:
2019-08-15 16:35:42 +00:00
unsupported "awaitne is only available within {.async.}"
2018-05-16 08:22:34 +00:00
macro async*(prc: untyped): untyped =
## Macro which processes async procedures into the appropriate
## iterators and yield statements.
if prc.kind == nnkStmtList:
for oneProc in prc:
result = newStmtList()
result.add asyncSingleProc(oneProc)
else:
result = asyncSingleProc(prc)
when defined(nimDumpAsync):
echo repr result