diff options
Diffstat (limited to 'lib/pure/asyncmacro.nim')
-rw-r--r-- | lib/pure/asyncmacro.nim | 389 |
1 files changed, 162 insertions, 227 deletions
diff --git a/lib/pure/asyncmacro.nim b/lib/pure/asyncmacro.nim index 11eba427b..d4e72c28a 100644 --- a/lib/pure/asyncmacro.nim +++ b/lib/pure/asyncmacro.nim @@ -7,104 +7,68 @@ # distribution, for details about the copyright. # -## `asyncdispatch` module depends on the `asyncmacro` module to work properly. +## Implements the `async` and `multisync` macros for `asyncdispatch`. -import macros, strutils, asyncfutures +import std/[macros, strutils, asyncfutures] -proc skipUntilStmtList(node: NimNode): NimNode {.compileTime.} = - # Skips a nest of StmtList's. - result = node - if node[0].kind == nnkStmtList: - result = skipUntilStmtList(node[0]) +type + Context = ref object + inTry: int + hasRet: bool -proc skipStmtList(node: NimNode): NimNode {.compileTime.} = - result = node - if node[0].kind == nnkStmtList: - result = node[0] +# 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 - let retFutUnown = unown retFutureSym - var nameIterVar = iteratorNameSym - proc identName {.closure.} = + proc identName {.closure, stackTrace: off.} = try: if not nameIterVar.finished: - var next = unown nameIterVar() + var next = nameIterVar() # Continue while the yielded future is already finished. while (not next.isNil) and next.finished: - next = unown nameIterVar() + next = nameIterVar() if nameIterVar.finished: break if next == nil: - if not retFutUnown.finished: - let msg = "Async procedure ($1) yielded `nil`, are you await'ing a " & - "`nil` Future?" - raise newException(AssertionError, msg % strName) + 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.}: - {.push hint[ConvFromXtoItselfNotNeeded]: off.} - next.callback = (proc() {.closure, gcsafe.})(identName) - {.pop.} + next.addCallback cast[proc() {.closure, gcsafe.}](identName) except: futureVarCompletions - if retFutUnown.finished: + 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: - retFutUnown.fail(getCurrentException()) + retFutureSym.fail(getCurrentException()) identName() -template useVar(result: var NimNode, futureVarNode: NimNode, valueReceiver, - rootReceiver: untyped, fromNode: NimNode) = - ## Params: - ## futureVarNode: The NimNode which is a symbol identifying the Future[T] - ## variable to yield. - ## fromNode: Used for better debug information (to give context). - ## valueReceiver: The node which defines an expression that retrieves the - ## future's value. - ## - ## rootReceiver: ??? TODO - # -> yield future<x> - result.add newNimNode(nnkYieldStmt, fromNode).add(futureVarNode) - # -> future<x>.read - valueReceiver = newDotExpr(futureVarNode, newIdentNode("read")) - result.add rootReceiver - -template createVar(result: var NimNode, futSymName: string, - asyncProc: NimNode, - valueReceiver, rootReceiver: untyped, - fromNode: NimNode) = - result = newNimNode(nnkStmtList, fromNode) - var futSym = genSym(nskVar, "future") - result.add newVarStmt(futSym, asyncProc) # -> var future<x> = y - useVar(result, futSym, valueReceiver, rootReceiver, fromNode) - -proc createFutureVarCompletions(futureVarIdents: seq[NimNode], - fromNode: NimNode): NimNode {.compileTime.} = +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. - # TODO: Once https://github.com/nim-lang/Nim/issues/5617 is fixed. - # TODO: Add line info to the complete() call! # 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"))), - newCall(newIdentNode("complete"), ident) + newCallWithLineInfo(fromNode, newIdentNode("complete"), ident) ) ) -proc processBody(node, retFutureSym: NimNode, - subTypeIsVoid: bool, - futureVarIdents: seq[NimNode]): NimNode {.compileTime.} = - #echo(node.treeRepr) +proc processBody(ctx: Context; node, needsCompletionSym, retFutureSym: NimNode, futureVarIdents: seq[NimNode]): NimNode = result = node case node.kind of nnkReturnStmt: @@ -113,76 +77,57 @@ proc processBody(node, retFutureSym: NimNode, # 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 not subTypeIsVoid: - result.add newCall(newIdentNode("complete"), retFutureSym, - newIdentNode("result")) + if ctx.inTry == 0: + result.add newCallWithLineInfo(node, newIdentNode("complete"), retFutureSym, newIdentNode("result")) else: - result.add newCall(newIdentNode("complete"), retFutureSym) + result.add newAssignment(needsCompletionSym, newLit(true)) else: - let x = node[0].processBody(retFutureSym, subTypeIsVoid, - futureVarIdents) + 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 newCall(newIdentNode("complete"), retFutureSym, x) + 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 nnkCommand, nnkCall: - if node[0].eqIdent("await"): - case node[1].kind - of nnkIdent, nnkInfix, nnkDotExpr, nnkCall, nnkCommand: - # await x - # await x or y - # await foo(p, x) - # await foo p, x - var futureValue: NimNode - result.createVar("future" & $node[1][0].toStrLit, node[1], futureValue, - futureValue, node) - else: - error("Invalid node kind in 'await', got: " & $node[1].kind) - elif node.len > 1 and node[1].kind == nnkCommand and - node[1][0].eqIdent("await"): - # foo await x - var newCommand = node - result.createVar("future" & $node[0].toStrLit, node[1][1], newCommand[1], - newCommand, node) - - of nnkVarSection, nnkLetSection: - case node[0][^1].kind - of nnkCommand: - if node[0][^1][0].eqIdent("await"): - # var x = await y - var newVarSection = node # TODO: Should this use copyNimNode? - result.createVar("future" & node[0][0].strVal, node[0][^1][1], - newVarSection[0][^1], newVarSection, node) - else: discard - of nnkAsgn: - case node[1].kind - of nnkCommand: - if node[1][0].eqIdent("await"): - # x = await y - var newAsgn = node - result.createVar("future" & $node[0].toStrLit, node[1][1], newAsgn[1], - newAsgn, node) - else: discard - of nnkDiscardStmt: - # discard await x - if node[0].kind == nnkCommand and - node[0][0].eqIdent("await"): - var newDiscard = node - result.createVar("futureDiscard_" & $toStrLit(node[0][1]), node[0][1], - newDiscard[0], newDiscard, node) of RoutineNodes-{nnkTemplateDef}: # skip all the nested procedure definitions return - else: discard + 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) - for i in 0 ..< result.len: - result[i] = processBody(result[i], retFutureSym, subTypeIsVoid, - futureVarIdents) + # echo result.repr -proc getName(node: NimNode): string {.compileTime.} = +proc getName(node: NimNode): string = case node.kind of nnkPostfix: return node[1].strVal @@ -191,30 +136,68 @@ proc getName(node: NimNode): string {.compileTime.} = of nnkEmpty: return "anonymous" else: - error("Unknown name.") + error("Unknown name.", node) -proc getFutureVarIdents(params: NimNode): seq[NimNode] {.compileTime.} = +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"): + 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) {.compileTime.} = +proc verifyReturnType(typeName: string, node: NimNode = nil) = if typeName.isInvalidReturnType: error("Expected return type of 'Future' got '$1'" % - typeName) + typeName, node) + +template await*(f: typed): untyped {.used.} = + static: + error "await expects Future[T], got " & $typeof(f) -proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = +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. + ## 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.") + " 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 @@ -226,29 +209,24 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = # Verify that the return type is a Future[T] if returnType.kind == nnkBracketExpr: let fut = repr(returnType[0]) - verifyReturnType(fut) + verifyReturnType(fut, returnType[0]) baseType = returnType[1] elif returnType.kind in nnkCallKinds and returnType[0].eqIdent("[]"): let fut = repr(returnType[1]) - verifyReturnType(fut) + verifyReturnType(fut, returnType[0]) baseType = returnType[2] elif returnType.kind == nnkEmpty: baseType = returnType else: - verifyReturnType(repr(returnType)) - - let subtypeIsVoid = returnType.kind == nnkEmpty or - (baseType.kind == nnkIdent and returnType[1].eqIdent("void")) + 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. - if prc.body.len > 1 and prc.body[0].kind == nnkCommentStmt: - outerProcBody.add(prc.body[0]) + let body2 = extractDocCommentsAndRunnables(prc.body) # -> var retFuture = newFuture[T]() var retFutureSym = genSym(nskVar, "retFuture") @@ -269,40 +247,42 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = # -> {.pop.} # -> <proc_body> # -> complete(retFuture, result) - var iteratorNameSym = genSym(nskIterator, $prcName & "Iter") - var procBody = prc.body.processBody(retFutureSym, subtypeIsVoid, - futureVarIdents) + 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.} - 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"), - retFutureSym, newIdentNode("result"))) # -> complete(retFuture, result) - else: - # -> complete(retFuture) - procBody.add(newCall(newIdentNode("complete"), retFutureSym)) + var `needsCompletionSym` = false + procBody.add quote do: + complete(`retFutureSym`, `resultIdent`) - var closureIterator = newProc(iteratorNameSym, [parseExpr("owned(FutureBase)")], + 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: + if prc.pragma.findChild(it.kind in {nnkSym, nnkIdent} and $it == "gcsafe") != nil: closureIterator.addPragma(newIdentNode("gcsafe")) outerProcBody.add(closureIterator) @@ -311,26 +291,26 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = # friendlier stack traces: var cbName = genSym(nskProc, prcName & NimAsyncContinueSuffix) var procCb = getAst createCb(retFutureSym, iteratorNameSym, - newStrLitNode(prcName), - cbName, - createFutureVarCompletions(futureVarIdents, nil)) + 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]) - if subtypeIsVoid: - # Add discardable pragma. - if returnType.kind == nnkEmpty: - # Add Future[void] - result.params[0] = parseExpr("owned(Future[void])") + # based on the yglukhov's patch to chronos: https://github.com/status-im/nim-chronos/pull/47 if procBody.kind != nnkEmpty: - result.body = outerProcBody - #echo(treeRepr(result)) - #if prcName == "recvLineInto": - # echo(toStrLit(result)) + body2.add quote do: + `outerProcBody` + result.body = body2 macro async*(prc: untyped): untyped = ## Macro which processes async procedures into the appropriate @@ -344,55 +324,11 @@ macro async*(prc: untyped): untyped = when defined(nimDumpAsync): echo repr result - -# Multisync -proc emptyNoop[T](x: T): T = - # The ``await``s are replaced by a call to this for simplicity. - when T isnot void: - return x - -proc stripAwait(node: NimNode): NimNode = - ## Strips out all ``await`` commands from a procedure body, replaces them - ## with ``emptyNoop`` for simplicity. - result = node - - let emptyNoopSym = bindSym("emptyNoop") - - case node.kind - of nnkCommand, nnkCall: - if node[0].eqIdent("await"): - node[0] = emptyNoopSym - elif node.len > 1 and node[1].kind == nnkCommand and node[1][0].eqIdent("await"): - # foo await x - node[1][0] = emptyNoopSym - of nnkVarSection, nnkLetSection: - case node[0][^1].kind - of nnkCommand: - if node[0][^1][0].eqIdent("await"): - # var x = await y - node[0][^1][0] = emptyNoopSym - else: discard - of nnkAsgn: - case node[1].kind - of nnkCommand: - if node[1][0].eqIdent("await"): - # x = await y - node[1][0] = emptyNoopSym - else: discard - of nnkDiscardStmt: - # discard await x - if node[0].kind == nnkCommand and node[0][0].eqIdent("await"): - node[0][0] = emptyNoopSym - else: discard - - for i in 0 ..< result.len: - result[i] = stripAwait(result[i]) - 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].strVal.normalize - let secondAsync = "async" in paramType[2].strVal.normalize + 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] @@ -404,14 +340,14 @@ proc stripReturnType(returnType: NimNode): NimNode = result = returnType if returnType.kind == nnkBracketExpr: let fut = repr(returnType[0]) - verifyReturnType(fut) + 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. + ## It transforms them so that `proc (socket: Socket)` and + ## `proc (socket: AsyncSocket)` are returned. result[0] = prc.copyNimTree() # Retrieve the `T` inside `Future[T]`. @@ -420,8 +356,12 @@ proc splitProc(prc: NimNode): (NimNode, NimNode) = 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) - result[0][6] = stripAwait(result[0][6]) + 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: @@ -435,14 +375,9 @@ 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. + ## 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) - -proc await*[T](x: T) = - ## The 'await' keyword is also defined here for technical - ## reasons. (Generic symbol lookup prepass.) - {.error: "Await only available within .async".} |