123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- #
- #
- # Nim's Runtime Library
- # (c) Copyright 2015 Dominik Picheta
- #
- # See the file "copying.txt", included in this
- # distribution, for details about the copyright.
- #
- ## Implements the `async` and `multisync` macros for `asyncdispatch`.
- import std/[macros, strutils, asyncfutures]
- type
- Context = ref object
- inTry: int
- hasRet: bool
- # TODO: Ref https://github.com/nim-lang/Nim/issues/5617
- # TODO: Add more line infos
- proc newCallWithLineInfo(fromNode: NimNode; theProc: NimNode, args: varargs[NimNode]): NimNode =
- result = newCall(theProc, args)
- result.copyLineInfo(fromNode)
- template createCb(retFutureSym, iteratorNameSym,
- strName, identName, futureVarCompletions: untyped) =
- bind finished
- var nameIterVar = iteratorNameSym
- proc identName {.closure, stackTrace: off.} =
- try:
- if not nameIterVar.finished:
- var next = nameIterVar()
- # Continue while the yielded future is already finished.
- while (not next.isNil) and next.finished:
- next = nameIterVar()
- if nameIterVar.finished:
- break
- if next == nil:
- if not retFutureSym.finished:
- let msg = "Async procedure ($1) yielded `nil`, are you await'ing a `nil` Future?"
- raise newException(AssertionDefect, msg % strName)
- else:
- {.gcsafe.}:
- next.addCallback cast[proc() {.closure, gcsafe.}](identName)
- except:
- futureVarCompletions
- if retFutureSym.finished:
- # Take a look at tasyncexceptions for the bug which this fixes.
- # That test explains it better than I can here.
- raise
- else:
- retFutureSym.fail(getCurrentException())
- identName()
- proc createFutureVarCompletions(futureVarIdents: seq[NimNode], fromNode: NimNode): NimNode =
- result = newNimNode(nnkStmtList, fromNode)
- # Add calls to complete each FutureVar parameter.
- for ident in futureVarIdents:
- # Only complete them if they have not been completed already by the user.
- # In the meantime, this was really useful for debugging :)
- #result.add(newCall(newIdentNode("echo"), newStrLitNode(fromNode.lineinfo)))
- result.add newIfStmt(
- (
- newCall(newIdentNode("not"),
- newDotExpr(ident, newIdentNode("finished"))),
- newCallWithLineInfo(fromNode, newIdentNode("complete"), ident)
- )
- )
- proc processBody(ctx: Context; node, needsCompletionSym, retFutureSym: NimNode, futureVarIdents: seq[NimNode]): NimNode =
- result = node
- case node.kind
- of nnkReturnStmt:
- result = newNimNode(nnkStmtList, node)
- # As I've painfully found out, the order here really DOES matter.
- result.add createFutureVarCompletions(futureVarIdents, node)
- ctx.hasRet = true
- if node[0].kind == nnkEmpty:
- if ctx.inTry == 0:
- result.add newCallWithLineInfo(node, newIdentNode("complete"), retFutureSym, newIdentNode("result"))
- else:
- result.add newAssignment(needsCompletionSym, newLit(true))
- else:
- let x = processBody(ctx, node[0], needsCompletionSym, retFutureSym, futureVarIdents)
- if x.kind == nnkYieldStmt: result.add x
- elif ctx.inTry == 0:
- result.add newCallWithLineInfo(node, newIdentNode("complete"), retFutureSym, x)
- else:
- result.add newAssignment(newIdentNode("result"), x)
- result.add newAssignment(needsCompletionSym, newLit(true))
- 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
- of nnkTryStmt:
- if result[^1].kind == nnkFinally:
- inc ctx.inTry
- result[0] = processBody(ctx, result[0], needsCompletionSym, retFutureSym, futureVarIdents)
- dec ctx.inTry
- for i in 1 ..< result.len:
- result[i] = processBody(ctx, result[i], needsCompletionSym, retFutureSym, futureVarIdents)
- if ctx.inTry == 0 and ctx.hasRet:
- let finallyNode = copyNimNode(result[^1])
- let stmtNode = newNimNode(nnkStmtList)
- for child in result[^1]:
- stmtNode.add child
- stmtNode.add newIfStmt(
- ( needsCompletionSym,
- newCallWithLineInfo(node, newIdentNode("complete"), retFutureSym,
- newIdentNode("result")
- )
- )
- )
- finallyNode.add stmtNode
- result[^1] = finallyNode
- else:
- for i in 0 ..< result.len:
- result[i] = processBody(ctx, result[i], needsCompletionSym, retFutureSym, futureVarIdents)
- else:
- for i in 0 ..< result.len:
- result[i] = processBody(ctx, result[i], needsCompletionSym, retFutureSym, futureVarIdents)
- # echo result.repr
- proc getName(node: NimNode): string =
- case node.kind
- of nnkPostfix:
- return node[1].strVal
- of nnkIdent, nnkSym:
- return node.strVal
- of nnkEmpty:
- return "anonymous"
- else:
- error("Unknown name.", node)
- proc getFutureVarIdents(params: NimNode): seq[NimNode] =
- result = @[]
- for i in 1 ..< len(params):
- expectKind(params[i], nnkIdentDefs)
- if params[i][1].kind == nnkBracketExpr and
- params[i][1][0].eqIdent(FutureVar.astToStr):
- ## eqIdent: first char is case sensitive!!!
- result.add(params[i][0])
- proc isInvalidReturnType(typeName: string): bool =
- return typeName notin ["Future"] #, "FutureStream"]
- proc verifyReturnType(typeName: string, node: NimNode = nil) =
- if typeName.isInvalidReturnType:
- error("Expected return type of 'Future' got '$1'" %
- typeName, node)
- template await*(f: typed): untyped {.used.} =
- static:
- error "await expects Future[T], got " & $typeof(f)
- template await*[T](f: Future[T]): auto {.used.} =
- when not defined(nimHasTemplateRedefinitionPragma):
- {.pragma: redefine.}
- template yieldFuture {.redefine.} = yield FutureBase()
- when compiles(yieldFuture):
- var internalTmpFuture: FutureBase = f
- yield internalTmpFuture
- (cast[typeof(f)](internalTmpFuture)).read()
- else:
- macro errorAsync(futureError: Future[T]) =
- error(
- "Can only 'await' inside a proc marked as 'async'. Use " &
- "'waitFor' when calling an 'async' proc in a non-async scope instead",
- futureError)
- errorAsync(f)
- proc asyncSingleProc(prc: NimNode): NimNode =
- ## This macro transforms a single procedure into a closure iterator.
- ## The `async` macro supports a stmtList holding multiple async procedures.
- if prc.kind == nnkProcTy:
- result = prc
- if prc[0][0].kind == nnkEmpty:
- result[0][0] = quote do: Future[void]
- return result
- if prc.kind in RoutineNodes and prc.name.kind != nnkEmpty:
- # Only non anonymous functions need/can have stack trace disabled
- prc.addPragma(nnkExprColonExpr.newTree(ident"stackTrace", ident"off"))
- if prc.kind notin {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
- error("Cannot transform this node kind into an async proc." &
- " proc/method definition or lambda node expected.", prc)
- if prc[4].kind != nnkEmpty:
- for prag in prc[4]:
- if prag.eqIdent("discardable"):
- error("Cannot make async proc discardable. Futures have to be " &
- "checked with `asyncCheck` instead of discarded", prag)
- let prcName = prc.name.getName
- var returnType = prc.params[0]
- var baseType: NimNode
- if returnType.kind in nnkCallKinds and returnType[0].eqIdent("owned") and
- returnType.len == 2:
- returnType = returnType[1]
- # Verify that the return type is a Future[T]
- if returnType.kind == nnkBracketExpr:
- let fut = repr(returnType[0])
- verifyReturnType(fut, returnType[0])
- baseType = returnType[1]
- elif returnType.kind in nnkCallKinds and returnType[0].eqIdent("[]"):
- let fut = repr(returnType[1])
- verifyReturnType(fut, returnType[0])
- baseType = returnType[2]
- elif returnType.kind == nnkEmpty:
- baseType = returnType
- else:
- verifyReturnType(repr(returnType), returnType)
- let futureVarIdents = getFutureVarIdents(prc.params)
- var outerProcBody = newNimNode(nnkStmtList, prc.body)
- # Extract the documentation comment from the original procedure declaration.
- # Note that we're not removing it from the body in order not to make this
- # transformation even more complex.
- let body2 = extractDocCommentsAndRunnables(prc.body)
- # -> var retFuture = newFuture[T]()
- var retFutureSym = genSym(nskVar, "retFuture")
- var subRetType =
- if returnType.kind == nnkEmpty: newIdentNode("void")
- else: baseType
- outerProcBody.add(
- newVarStmt(retFutureSym,
- newCall(
- newNimNode(nnkBracketExpr, prc.body).add(
- newIdentNode("newFuture"),
- subRetType),
- newLit(prcName)))) # Get type from return type of this proc
- # -> iterator nameIter(): FutureBase {.closure.} =
- # -> {.push warning[resultshadowed]: off.}
- # -> var result: T
- # -> {.pop.}
- # -> <proc_body>
- # -> complete(retFuture, result)
- var iteratorNameSym = genSym(nskIterator, $prcName & " (Async)")
- var needsCompletionSym = genSym(nskVar, "needsCompletion")
- var ctx = Context()
- var procBody = processBody(ctx, prc.body, needsCompletionSym, retFutureSym, futureVarIdents)
- # don't do anything with forward bodies (empty)
- if procBody.kind != nnkEmpty:
- # fix #13899, defer should not escape its original scope
- let blockStmt = newStmtList(newTree(nnkBlockStmt, newEmptyNode(), procBody))
- procBody = newStmtList()
- let resultIdent = ident"result"
- procBody.add quote do:
- # Check whether there is an implicit return
- when typeof(`blockStmt`) is void:
- `blockStmt`
- else:
- `resultIdent` = `blockStmt`
- procBody.add(createFutureVarCompletions(futureVarIdents, nil))
- procBody.insert(0): quote do:
- {.push warning[resultshadowed]: off.}
- when `subRetType` isnot void:
- var `resultIdent`: `subRetType`
- else:
- var `resultIdent`: Future[void]
- {.pop.}
- var `needsCompletionSym` = false
- procBody.add quote do:
- complete(`retFutureSym`, `resultIdent`)
- var closureIterator = newProc(iteratorNameSym, [quote do: owned(FutureBase)],
- procBody, nnkIteratorDef)
- closureIterator.pragma = newNimNode(nnkPragma, lineInfoFrom = prc.body)
- closureIterator.addPragma(newIdentNode("closure"))
- # 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 == "gcsafe") != nil:
- closureIterator.addPragma(newIdentNode("gcsafe"))
- outerProcBody.add(closureIterator)
- # -> createCb(retFuture)
- # NOTE: The NimAsyncContinueSuffix is checked for in asyncfutures.nim to produce
- # friendlier stack traces:
- var cbName = genSym(nskProc, prcName & NimAsyncContinueSuffix)
- var procCb = getAst createCb(retFutureSym, iteratorNameSym,
- newStrLitNode(prcName),
- cbName,
- createFutureVarCompletions(futureVarIdents, nil)
- )
- outerProcBody.add procCb
- # -> return retFuture
- outerProcBody.add newNimNode(nnkReturnStmt, prc.body[^1]).add(retFutureSym)
- result = prc
- # Add discardable pragma.
- if returnType.kind == nnkEmpty:
- # xxx consider removing `owned`? it's inconsistent with non-void case
- result.params[0] = quote do: owned(Future[void])
- # based on the yglukhov's patch to chronos: https://github.com/status-im/nim-chronos/pull/47
- if procBody.kind != nnkEmpty:
- body2.add quote do:
- `outerProcBody`
- result.body = body2
- 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 defined(nimDumpAsync):
- echo repr result
- proc splitParamType(paramType: NimNode, async: bool): NimNode =
- result = paramType
- if paramType.kind == nnkInfix and paramType[0].strVal in ["|", "or"]:
- let firstAsync = "async" in paramType[1].toStrLit().strVal.normalize
- let secondAsync = "async" in paramType[2].toStrLit().strVal.normalize
- if firstAsync:
- result = paramType[if async: 1 else: 2]
- elif secondAsync:
- result = paramType[if async: 2 else: 1]
- proc stripReturnType(returnType: NimNode): NimNode =
- # Strip out the 'Future' from 'Future[T]'.
- result = returnType
- if returnType.kind == nnkBracketExpr:
- let fut = repr(returnType[0])
- verifyReturnType(fut, returnType)
- result = returnType[1]
- proc splitProc(prc: NimNode): (NimNode, NimNode) =
- ## Takes a procedure definition which takes a generic union of arguments,
- ## for example: proc (socket: Socket | AsyncSocket).
- ## It transforms them so that `proc (socket: Socket)` and
- ## `proc (socket: AsyncSocket)` are returned.
- result[0] = prc.copyNimTree()
- # Retrieve the `T` inside `Future[T]`.
- let returnType = stripReturnType(result[0][3][0])
- result[0][3][0] = splitParamType(returnType, async = false)
- for i in 1 ..< result[0][3].len:
- # Sync proc (0) -> FormalParams (3) -> IdentDefs, the parameter (i) ->
- # parameter type (1).
- result[0][3][i][1] = splitParamType(result[0][3][i][1], async=false)
- var multisyncAwait = quote:
- template await(value: typed): untyped =
- value
- result[0][^1] = nnkStmtList.newTree(multisyncAwait, result[0][^1])
- result[1] = prc.copyNimTree()
- if result[1][3][0].kind == nnkBracketExpr:
- result[1][3][0][1] = splitParamType(result[1][3][0][1], async = true)
- for i in 1 ..< result[1][3].len:
- # Async proc (1) -> FormalParams (3) -> IdentDefs, the parameter (i) ->
- # parameter type (1).
- result[1][3][i][1] = splitParamType(result[1][3][i][1], async = true)
- macro multisync*(prc: untyped): untyped =
- ## Macro which processes async procedures into both asynchronous and
- ## synchronous procedures.
- ##
- ## The generated async procedures use the `async` macro, whereas the
- ## generated synchronous procedures simply strip off the `await` calls.
- let (sync, asyncPrc) = splitProc(prc)
- result = newStmtList()
- result.add(asyncSingleProc(asyncPrc))
- result.add(sync)
|