diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index f5b3570..a7fc3ba 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -9,46 +9,73 @@ import std/[macros] -proc skipUntilStmtList(node: NimNode): NimNode {.compileTime.} = - # Skips a nest of StmtList's. - if node[0].kind == nnkStmtList: - skipUntilStmtList(node[0]) +# `quote do` will ruin line numbers so we avoid it using these helpers +proc completeWithResult(fut, baseType: NimNode): NimNode {.compileTime.} = + # when `baseType` is void: + # complete(`fut`) + # else: + # complete(`fut`, result) + if baseType.eqIdent("void"): + # Shortcut if we know baseType at macro expansion time + newCall(ident "complete", fut) else: - node + # `baseType` might be generic and resolve to `void` + nnkWhenStmt.newTree( + nnkElifExpr.newTree( + nnkInfix.newTree(ident "is", baseType, ident "void"), + newCall(ident "complete", fut) + ), + nnkElseExpr.newTree( + newCall(ident "complete", fut, ident "result") + ) + ) -proc processBody(node, retFutureSym: NimNode, - baseTypeIsVoid: bool): NimNode {.compileTime.} = +proc completeWithNode(fut, baseType, node: NimNode): NimNode {.compileTime.} = + # when typeof(`node`) is void: + # `node` # statement / explicit return + # -> completeWithResult(fut, baseType) + # else: # expression / implicit return + # complete(`fut`, `node`) + if node.kind == nnkEmpty: # shortcut when known at macro expanstion time + completeWithResult(fut, baseType) + else: + # Handle both expressions and statements - since the type is not know at + # macro expansion time, we delegate this choice to a later compilation stage + # with `when`. + nnkWhenStmt.newTree( + nnkElifExpr.newTree( + nnkInfix.newTree( + ident "is", nnkTypeOfExpr.newTree(node), ident "void"), + newStmtList( + node, + completeWithResult(fut, baseType) + ) + ), + nnkElseExpr.newTree( + newCall(ident "complete", fut, node) + ) + ) + +proc processBody(node, fut, baseType: NimNode): NimNode {.compileTime.} = #echo(node.treeRepr) - result = node case node.kind of nnkReturnStmt: - result = newNimNode(nnkStmtList, node) + let + res = newNimNode(nnkStmtList, node) + res.add completeWithNode(fut, baseType, processBody(node[0], fut, baseType)) + res.add newNimNode(nnkReturnStmt, node).add(newNilLit()) - # As I've painfully found out, the order here really DOES matter. - if node[0].kind == nnkEmpty: - if not baseTypeIsVoid: - result.add newCall(newIdentNode("complete"), retFutureSym, - newIdentNode("result")) - else: - result.add newCall(newIdentNode("complete"), retFutureSym) - else: - let x = node[0].processBody(retFutureSym, baseTypeIsVoid) - 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 + res of RoutineNodes-{nnkTemplateDef}: # skip all the nested procedure definitions - return node - 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`. - result[i] = processBody(result[i], retFutureSym, baseTypeIsVoid) + node + else: + for i in 0 ..< node.len: + # We must not transform nested procedures of any form, otherwise + # `fut` will be used for all nested procedures as their own + # `retFuture`. + node[i] = processBody(node[i], fut, baseType) + node proc getName(node: NimNode): string {.compileTime.} = case node.kind @@ -63,10 +90,6 @@ proc getName(node: NimNode): string {.compileTime.} = else: error("Unknown name.") -proc verifyReturnType(typeName: string) {.compileTime.} = - if typeName != "Future": - error("Expected return type of 'Future' got '" & typeName & "'") - macro unsupported(s: static[string]): untyped = error s @@ -95,22 +118,25 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = ## 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." & - " proc/method definition or lambda node expected.") + " proc/method definition or lambda node expected.", prc) let returnType = cleanupOpenSymChoice(prc.params2[0]) # Verify that the return type is a Future[T] let baseType = - if returnType.kind == nnkBracketExpr: - let fut = repr(returnType[0]) - verifyReturnType(fut) - returnType[1] - elif returnType.kind == nnkEmpty: - ident("void") + if returnType.kind == nnkEmpty: + ident "void" + elif not ( + returnType.kind == nnkBracketExpr and eqIdent(returnType[0], "Future")): + error( + "Expected return type of 'Future' got '" & repr(returnType) & "'", prc) + return else: - raiseAssert("Unhandled async return type: " & $prc.kind) + returnType[1] - let baseTypeIsVoid = baseType.eqIdent("void") + let + baseTypeIsVoid = baseType.eqIdent("void") + futureVoidType = nnkBracketExpr.newTree(ident "Future", ident "void") if prc.kind in {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}: let @@ -124,63 +150,50 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = let internalFutureSym = ident "chronosInternalRetFuture" internalFutureType = - if baseTypeIsVoid: - newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(newIdentNode("void")) + if baseTypeIsVoid: futureVoidType else: returnType - castFutureSym = quote do: - cast[`internalFutureType`](`internalFutureSym`) - procBody = prc.body.processBody(castFutureSym, baseTypeIsVoid) + castFutureSym = nnkCast.newTree(internalFutureType, internalFutureSym) + + procBody = prc.body.processBody(castFutureSym, baseType) # don't do anything with forward bodies (empty) if procBody.kind != nnkEmpty: - # fix #13899, `defer` should not escape its original scope - let procBodyBlck = - newStmtList(newTree(nnkBlockStmt, newEmptyNode(), procBody)) - - # Avoid too much quote do to not lose original line numbers - let closureBody = if baseTypeIsVoid: - let resultTemplate = quote do: - template result: auto {.used.} = - {.fatal: "You should not reference the `result` variable inside" & - " a void async proc".} - # -> complete(chronosInternalRetFuture) - let complete = - newCall(newIdentNode("complete"), castFutureSym) - - newStmtList(resultTemplate, procBodyBlck, complete) - else: - # -> iterator nameIter(chronosInternalRetFuture: Future[T]): FutureBase {.closure.} = - # -> {.push warning[resultshadowed]: off.} - # -> var result: T - # -> {.pop.} - # -> - # -> complete(chronosInternalRetFuture, result) - newStmtList( - # -> {.push warning[resultshadowed]: off.} - newNimNode(nnkPragma).add(newIdentNode("push"), - newNimNode(nnkExprColonExpr).add(newNimNode(nnkBracketExpr).add( - newIdentNode("warning"), newIdentNode("resultshadowed")), - newIdentNode("off"))), - - # -> var result: T - newNimNode(nnkVarSection, prc.body).add( - newIdentDefs(newIdentNode("result"), baseType)), - - # -> {.pop.}) - newNimNode(nnkPragma).add( - newIdentNode("pop")), - - procBodyBlck, - - # -> complete(chronosInternalRetFuture, result) - newCall(newIdentNode("complete"), - castFutureSym, newIdentNode("result"))) - let - internalFutureParameter = nnkIdentDefs.newTree(internalFutureSym, newIdentNode("FutureBase"), newEmptyNode()) + # fix #13899, `defer` should not escape its original scope + procBodyBlck = nnkBlockStmt.newTree(newEmptyNode(), procBody) + + resultDecl = nnkWhenStmt.newTree( + # when `baseType` is void: + nnkElifExpr.newTree( + nnkInfix.newTree(ident "is", baseType, ident "void"), + quote do: + template result: auto {.used.} = + {.fatal: "You should not reference the `result` variable inside" & + " a void async proc".} + ), + # else: + nnkElseExpr.newTree( + newStmtList( + quote do: {.push warning[resultshadowed]: off.}, + # var result: `baseType` + nnkVarSection.newTree( + nnkIdentDefs.newTree(ident "result", baseType, newEmptyNode())), + quote do: {.pop.}, + ) + ) + ) + + completeDecl = completeWithNode(castFutureSym, baseType, procBodyBlck) + + closureBody = newStmtList(resultDecl, completeDecl) + + internalFutureParameter = nnkIdentDefs.newTree( + internalFutureSym, newIdentNode("FutureBase"), newEmptyNode()) iteratorNameSym = genSym(nskIterator, $prcName) - closureIterator = newProc(iteratorNameSym, [newIdentNode("FutureBase"), internalFutureParameter], - closureBody, nnkIteratorDef) + closureIterator = newProc( + iteratorNameSym, + [newIdentNode("FutureBase"), internalFutureParameter], + closureBody, nnkIteratorDef) iteratorNameSym.copyLineInfo(prc) @@ -221,6 +234,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = # doesn't reference it, avoid cyclic ref (#203) let retFutureSym = ident "resultFuture" + retFutureSym.copyLineInfo(prc) # Do not change this code to `quote do` version because `instantiationInfo` # will be broken for `newFuture()` call. outerProcBody.add( @@ -230,7 +244,6 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = newLit(prcName)) ) ) - # -> resultFuture.closure = iterator outerProcBody.add( newAssignment( @@ -266,10 +279,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = if baseTypeIsVoid: if returnType.kind == nnkEmpty: # Add Future[void] - prc.params2[0] = - newNimNode(nnkBracketExpr, prc) - .add(newIdentNode("Future")) - .add(newIdentNode("void")) + prc.params2[0] = futureVoidType prc diff --git a/tests/testmacro.nim b/tests/testmacro.nim index 680a2ba..2526c5d 100644 --- a/tests/testmacro.nim +++ b/tests/testmacro.nim @@ -5,8 +5,8 @@ # Licensed under either of # Apache License, version 2.0, (LICENSE-APACHEv2) # MIT license (LICENSE-MIT) +import std/[macros, strutils] import unittest2 -import macros import ../chronos {.used.} @@ -123,6 +123,76 @@ suite "Macro transformations test suite": macroAsync2(testMacro2, seq, Opt, Result, OpenObject, cstring) check waitFor(testMacro2()).len == 0 + test "Future with generics": + proc gen(T: typedesc): Future[T] {.async.} = + proc testproc(): Future[T] {.async.} = + when T is void: + return + else: + return default(T) + await testproc() + + waitFor gen(void) + check: + waitFor(gen(int)) == default(int) + + test "Implicit return": + proc implicit(): Future[int] {.async.} = + 42 + + proc implicit2(): Future[int] {.async.} = + block: + 42 + + proc implicit3(): Future[int] {.async.} = + try: + parseInt("error") + except ValueError: + 42 + + proc implicit4(v: bool): Future[int] {.async.} = + case v + of false: 5 + of true: 42 + + proc implicit5(v: bool): Future[int] {.async.} = + if v: 42 + else: 5 + + proc implicit6(v: ref int): Future[int] {.async.} = + try: + parseInt("error") + except ValueError: + 42 + finally: + v[] = 42 + + proc implicit7(v: bool): Future[int] {.async.} = + case v + of false: return 33 + of true: 42 + + proc implicit8(v: bool): Future[int] {.async.} = + case v + of false: await implicit7(v) + of true: 42 + + let fin = new int + check: + waitFor(implicit()) == 42 + waitFor(implicit2()) == 42 + waitFor(implicit3()) == 42 + waitFor(implicit4(true)) == 42 + waitFor(implicit5(true)) == 42 + waitFor(implicit5(false)) == 5 + waitFor(implicit6(fin)) == 42 + fin[] == 42 + waitFor(implicit7(true)) == 42 + waitFor(implicit7(false)) == 33 + + waitFor(implicit8(true)) == 42 + waitFor(implicit8(false)) == 33 + suite "Closure iterator's exception transformation issues": test "Nested defer/finally not called on return": # issue #288