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

View File

@ -88,13 +88,12 @@ proc cleanupOpenSymChoice(node: NimNode): NimNode {.compileTime.} =
proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
## This macro transforms a single procedure into a closure iterator. ## This macro transforms a single procedure into a closure iterator.
## The ``async`` macro supports a stmtList holding multiple async procedures. ## The ``async`` macro supports a stmtList holding multiple async procedures.
if prc.kind notin {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}: if prc.kind notin {nnkProcTy, nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
error("Cannot transform this node kind into an async proc." & error("Cannot transform " & $prc.kind & " into an async proc." &
" proc/method definition or lambda node expected.") " proc/method definition or lambda node expected.")
let prcName = prc.name.getName let returnType =
cleanupOpenSymChoice(if prc.kind == nnkProcTy: prc[0][0] else: prc.params[0])
let returnType = cleanupOpenSymChoice(prc.params[0])
var baseType: NimNode var baseType: NimNode
# Verify that the return type is a Future[T] # Verify that the return type is a Future[T]
if returnType.kind == nnkBracketExpr: if returnType.kind == nnkBracketExpr:
@ -109,150 +108,165 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
let subtypeIsVoid = returnType.kind == nnkEmpty or let subtypeIsVoid = returnType.kind == nnkEmpty or
(baseType.kind == nnkIdent and returnType[1].eqIdent("void")) (baseType.kind == nnkIdent and returnType[1].eqIdent("void"))
var outerProcBody = newNimNode(nnkStmtList, prc.body) if prc.kind in {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
# 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))
let let
internalFutureType = prcName = prc.name.getName
if subtypeIsVoid: outerProcBody = newNimNode(nnkStmtList, prc.body)
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"))
# TODO when push raises is active in a module, the iterator here inherits # Copy comment for nimdoc
# that annotation - here we explicitly disable it again which goes if prc.body.len > 0 and prc.body[0].kind == nnkCommentStmt:
# against the spirit of the raises annotation - one should investigate outerProcBody.add(prc.body[0])
# 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
))
# If proc has an explicit gcsafe pragma, we add it to iterator as well. # -> iterator nameIter(chronosInternalRetFuture: Future[T]): FutureBase {.closure.} =
if prc.pragma.findChild(it.kind in {nnkSym, nnkIdent} and # -> {.push warning[resultshadowed]: off.}
it.strVal == "gcsafe") != nil: # -> var result: T
closureIterator.addPragma(newIdentNode("gcsafe")) # -> {.pop.}
outerProcBody.add(closureIterator) # -> <proc_body>
# -> complete(chronosInternalRetFuture, result)
let
internalFutureSym = ident "chronosInternalRetFuture"
iteratorNameSym = genSym(nskIterator, $prcName)
var
procBody = prc.body.processBody(internalFutureSym, subtypeIsVoid)
# -> var resultFuture = newFuture[T]() # don't do anything with forward bodies (empty)
# declared at the end to be sure that the closure if procBody.kind != nnkEmpty:
# doesn't reference it, avoid cyclic ref (#203) if subtypeIsVoid:
var retFutureSym = ident "resultFuture" let resultTemplate = quote do:
var subRetType = template result: auto {.used.} =
if returnType.kind == nnkEmpty: {.fatal: "You should not reference the `result` variable inside" &
newIdentNode("void") " 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: else:
baseType # -> complete(chronosInternalRetFuture)
# Do not change this code to `quote do` version because `instantiationInfo` procBody.add(newCall(newIdentNode("complete"), internalFutureSym))
# will be broken for `newFuture()` call.
outerProcBody.add( let
newVarStmt( internalFutureType =
retFutureSym, if subtypeIsVoid:
newCall(newTree(nnkBracketExpr, ident "newFuture", subRetType), newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(newIdentNode("void"))
newLit(prcName)) 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 # -> resultFuture.closure = iterator
outerProcBody.add( outerProcBody.add(
newAssignment( newAssignment(
newDotExpr(retFutureSym, newIdentNode("closure")), newDotExpr(retFutureSym, newIdentNode("closure")),
iteratorNameSym) iteratorNameSym)
) )
# -> futureContinue(resultFuture)) # -> futureContinue(resultFuture))
outerProcBody.add( outerProcBody.add(
newCall(newIdentNode("futureContinue"), retFutureSym) newCall(newIdentNode("futureContinue"), retFutureSym)
) )
# -> return resultFuture # -> return resultFuture
outerProcBody.add newNimNode(nnkReturnStmt, prc.body[^1]).add(retFutureSym) outerProcBody.add newNimNode(nnkReturnStmt, prc.body[^1]).add(retFutureSym)
if prc.kind != nnkLambda: # TODO: Nim bug? prc.body = outerProcBody
if prc.kind notin {nnkProcTy, nnkLambda}: # TODO: Nim bug?
prc.addPragma(newColonExpr(ident "stackTrace", ident "off")) prc.addPragma(newColonExpr(ident "stackTrace", ident "off"))
# See **Remark 435** in this file. # See **Remark 435** in this file.
# https://github.com/nim-lang/RFCs/issues/435 # https://github.com/nim-lang/RFCs/issues/435
prc.addPragma(newIdentNode("gcsafe")) 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: if subtypeIsVoid:
# Add discardable pragma. # Add discardable pragma.
if returnType.kind == nnkEmpty: if returnType.kind == nnkEmpty:
# Add Future[void] # Add Future[void]
result.params[0] = prc.params[0] =
newNimNode(nnkBracketExpr, prc) newNimNode(nnkBracketExpr, prc)
.add(newIdentNode("Future")) .add(newIdentNode("Future"))
.add(newIdentNode("void")) .add(newIdentNode("void"))
if procBody.kind != nnkEmpty:
result.body = outerProcBody prc
#echo(treeRepr(result)) #echo(treeRepr(result))
#if prcName == "recvLineInto":
# echo(toStrLit(result))
template await*[T](f: Future[T]): untyped = template await*[T](f: Future[T]): untyped =
when declared(chronosInternalRetFuture): when declared(chronosInternalRetFuture):

View File

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