async proc types (#346)

Allow `type = proc(...) {.async.}` for pre-declaring async callback
types - in this case, the macro applies the correct transformation to
the return type and adds the appropriate exception / gcsafe annotations
This commit is contained in:
Jacek Sieka 2023-01-18 15:54:39 +01:00 committed by GitHub
parent 945c304197
commit c65cc4c136
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 161 additions and 134 deletions

View File

@ -221,7 +221,7 @@ const
SentinelCallback = AsyncCallback(function: sentinelCallbackImpl,
udata: nil)
proc isSentinel(acb: AsyncCallback): bool {.raises: [Defect].} =
proc isSentinel(acb: AsyncCallback): bool =
acb == SentinelCallback
proc `<`(a, b: TimerCallback): bool =
@ -896,22 +896,22 @@ proc removeTimer*(at: uint64, cb: CallbackFunc, udata: pointer = nil) {.
inline, deprecated: "Use removeTimer(Duration, cb, udata)".} =
removeTimer(Moment.init(int64(at), Millisecond), cb, udata)
proc callSoon*(acb: AsyncCallback) {.gcsafe, raises: [Defect].} =
proc callSoon*(acb: AsyncCallback) =
## Schedule `cbproc` to be called as soon as possible.
## The callback is called when control returns to the event loop.
getThreadDispatcher().callbacks.addLast(acb)
proc callSoon*(cbproc: CallbackFunc, data: pointer) {.
gcsafe, raises: [Defect].} =
gcsafe.} =
## Schedule `cbproc` to be called as soon as possible.
## The callback is called when control returns to the event loop.
doAssert(not isNil(cbproc))
callSoon(AsyncCallback(function: cbproc, udata: data))
proc callSoon*(cbproc: CallbackFunc) {.gcsafe, raises: [Defect].} =
proc callSoon*(cbproc: CallbackFunc) =
callSoon(cbproc, nil)
proc callIdle*(acb: AsyncCallback) {.gcsafe, raises: [Defect].} =
proc callIdle*(acb: AsyncCallback) =
## Schedule ``cbproc`` to be called when there no pending network events
## available.
##
@ -920,8 +920,7 @@ proc callIdle*(acb: AsyncCallback) {.gcsafe, raises: [Defect].} =
## actually "idle".
getThreadDispatcher().idlers.addLast(acb)
proc callIdle*(cbproc: CallbackFunc, data: pointer) {.
gcsafe, raises: [Defect].} =
proc callIdle*(cbproc: CallbackFunc, data: pointer) =
## Schedule ``cbproc`` to be called when there no pending network events
## available.
##
@ -931,7 +930,7 @@ proc callIdle*(cbproc: CallbackFunc, data: pointer) {.
doAssert(not isNil(cbproc))
callIdle(AsyncCallback(function: cbproc, udata: data))
proc callIdle*(cbproc: CallbackFunc) {.gcsafe, raises: [Defect].} =
proc callIdle*(cbproc: CallbackFunc) =
callIdle(cbproc, nil)
include asyncfutures2

View File

@ -88,13 +88,12 @@ proc cleanupOpenSymChoice(node: NimNode): NimNode {.compileTime.} =
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 {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
error("Cannot transform this node kind into an async proc." &
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.")
let prcName = prc.name.getName
let returnType = cleanupOpenSymChoice(prc.params[0])
let returnType =
cleanupOpenSymChoice(if prc.kind == nnkProcTy: prc[0][0] else: prc.params[0])
var baseType: NimNode
# Verify that the return type is a Future[T]
if returnType.kind == nnkBracketExpr:
@ -109,150 +108,165 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
let subtypeIsVoid = returnType.kind == nnkEmpty or
(baseType.kind == nnkIdent and returnType[1].eqIdent("void"))
var 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"
var 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))
if prc.kind in {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
let
internalFutureType =
if subtypeIsVoid:
newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(newIdentNode("void"))
else: returnType
internalFutureParameter = nnkIdentDefs.newTree(internalFutureSym, internalFutureType, newEmptyNode())
var closureIterator = newProc(iteratorNameSym, [newIdentNode("FutureBase"), internalFutureParameter],
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.
closureIterator.addPragma(newIdentNode("gcsafe"))
prcName = prc.name.getName
outerProcBody = newNimNode(nnkStmtList, prc.body)
# 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()
raises.add(newIdentNode("CatchableError"))
when not defined(chronosStrictException):
raises.add(newIdentNode("Exception"))
when (NimMajor, NimMinor) < (1, 4):
raises.add(newIdentNode("Defect"))
closureIterator.addPragma(nnkExprColonExpr.newTree(
newIdentNode("raises"),
raises
))
# Copy comment for nimdoc
if prc.body.len > 0 and prc.body[0].kind == nnkCommentStmt:
outerProcBody.add(prc.body[0])
# 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)
# -> 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)
# -> 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")
# 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:
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))
# -> 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],
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.
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)
)
# -> resultFuture.closure = iterator
outerProcBody.add(
newAssignment(
newDotExpr(retFutureSym, newIdentNode("closure")),
iteratorNameSym)
)
# -> return resultFuture
outerProcBody.add newNimNode(nnkReturnStmt, prc.body[^1]).add(retFutureSym)
# -> futureContinue(resultFuture))
outerProcBody.add(
newCall(newIdentNode("futureContinue"), retFutureSym)
)
if prc.kind != nnkLambda: # TODO: Nim bug?
# -> 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"))
result = prc
let raises = nnkBracket.newTree()
when (NimMajor, NimMinor) < (1, 4):
raises.add(newIdentNode("Defect"))
prc.addPragma(nnkExprColonExpr.newTree(
newIdentNode("raises"),
raises
))
if subtypeIsVoid:
# Add discardable pragma.
if returnType.kind == nnkEmpty:
# Add Future[void]
result.params[0] =
prc.params[0] =
newNimNode(nnkBracketExpr, prc)
.add(newIdentNode("Future"))
.add(newIdentNode("void"))
if procBody.kind != nnkEmpty:
result.body = outerProcBody
prc
#echo(treeRepr(result))
#if prcName == "recvLineInto":
# echo(toStrLit(result))
template await*[T](f: Future[T]): untyped =
when declared(chronosInternalRetFuture):

View File

@ -11,6 +11,10 @@ import ../chronos
when defined(nimHasUsed): {.used.}
type
RetValueType = proc(n: int): Future[int] {.async.}
RetVoidType = proc(n: int): Future[void] {.async.}
proc asyncRetValue(n: int): Future[int] {.async.} =
await sleepAsync(n.milliseconds)
result = n * 10
@ -31,6 +35,7 @@ proc asyncRetExceptionVoid(n: int) {.async.} =
proc testAwait(): Future[bool] {.async.} =
var res: int
await asyncRetVoid(100)
res = await asyncRetValue(100)
if res != 1000:
@ -50,6 +55,15 @@ proc testAwait(): Future[bool] {.async.} =
discard
if res != 0:
return false
block:
let fn: RetVoidType = asyncRetVoid
await fn(100)
block:
let fn: RetValueType = asyncRetValue
if (await fn(100)) != 1000:
return false
return true
proc testAwaitne(): Future[bool] {.async.} =