From 473a33c90f0c3e38ffee9b3c601acdb05320f10e Mon Sep 17 00:00:00 2001 From: Tanguy Date: Mon, 3 Jan 2022 15:09:18 +0100 Subject: [PATCH 01/31] Exception tracking v2 --- chronos/asyncfutures2.nim | 22 +++++++-- chronos/asyncloop.nim | 4 +- chronos/asyncmacro2.nim | 97 ++++++++++++++++++++++++++------------- 3 files changed, 84 insertions(+), 39 deletions(-) diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index b873292e1..9c03390e2 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -53,13 +53,12 @@ type # the future can be stored within the caller's stack frame. # How much refactoring is needed to make this a regular non-ref type? # Obviously, it will still be allocated on the heap when necessary. - Future*[T] = ref object of FutureBase ## Typed future. - when defined(chronosStrictException): - closure*: iterator(f: Future[T]): FutureBase {.raises: [Defect, CatchableError], gcsafe.} - else: - closure*: iterator(f: Future[T]): FutureBase {.raises: [Defect, CatchableError, Exception], gcsafe.} + FuturEx*[T, E] = ref object of FutureBase ## Typed future. + closure*: iterator(f: FuturEx[T, E]): FutureBase {.raises: [Defect, CatchableError, Exception], gcsafe.} value: T ## Stored value + Future*[T] = FuturEx[T, (CatchableError)] + FutureStr*[T] = ref object of Future[T] ## Future to hold GC strings gcholder*: string @@ -109,6 +108,9 @@ template setupFutureBase(loc: ptr SrcLoc) = proc newFutureImpl[T](loc: ptr SrcLoc): Future[T] = setupFutureBase(loc) +proc newFuturExImpl[T, E](loc: ptr SrcLoc): FuturEx[T, E] = + setupFutureBase(loc) + proc newFutureSeqImpl[A, B](loc: ptr SrcLoc): FutureSeq[A, B] = setupFutureBase(loc) @@ -122,6 +124,16 @@ template newFuture*[T](fromProc: static[string] = ""): Future[T] = ## that this future belongs to, is a good habit as it helps with debugging. newFutureImpl[T](getSrcLocation(fromProc)) +template newFuturEx*[T, E](fromProc: static[string] = ""): FuturEx[T, E] = + ## Creates a new future. + ## + ## Specifying ``fromProc``, which is a string specifying the name of the proc + ## that this future belongs to, is a good habit as it helps with debugging. + newFuturExImpl[T, E](getSrcLocation(fromProc)) + +converter toFuture*[T, E](futurex: FuturEx[T, E]): Future[T] = + cast[Future[T]](futurex) + template newFutureSeq*[A, B](fromProc: static[string] = ""): FutureSeq[A, B] = ## Create a new future which can hold/preserve GC sequence until future will ## not be completed. diff --git a/chronos/asyncloop.nim b/chronos/asyncloop.nim index f731e73d0..c550a2a5f 100644 --- a/chronos/asyncloop.nim +++ b/chronos/asyncloop.nim @@ -868,10 +868,10 @@ proc callIdle*(cbproc: CallbackFunc) {.gcsafe, raises: [Defect].} = include asyncfutures2 -proc sleepAsync*(duration: Duration): Future[void] = +proc sleepAsync*(duration: Duration): FuturEx[void, (CancelledError,)] = ## Suspends the execution of the current async procedure for the next ## ``duration`` time. - var retFuture = newFuture[void]("chronos.sleepAsync(Duration)") + var retFuture = newFuturEx[void, (CancelledError,)]("chronos.sleepAsync(Duration)") let moment = Moment.fromNow(duration) var timer: TimerCallback diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 04fda6ac4..cde0a3bfd 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -81,6 +81,21 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = let prcName = prc.name.getName + var + possibleExceptions = nnkBracket.newTree(newIdentNode("CancelledError")) + possibleExceptionsTuple = nnkTupleConstr.newTree(newIdentNode("CancelledError")) + foundRaises = false + for pragma in pragma(prc): + if pragma[0] == ident "raises": + foundRaises = true + for possibleRaise in pragma[1]: + possibleExceptions.add(possibleRaise) + possibleExceptionsTuple.add(possibleRaise) + break + if not foundRaises: + possibleExceptions.add(ident "CatchableError") + possibleExceptionsTuple.add(ident "CatchableError") + let returnType = prc.params[0] var baseType: NimNode # Verify that the return type is a Future[T] @@ -148,7 +163,9 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = if subtypeIsVoid: newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(newIdentNode("void")) else: returnType - internalFutureParameter = nnkIdentDefs.newTree(internalFutureSym, internalFutureType, newEmptyNode()) + returnTypeWithException = newNimNode(nnkBracketExpr).add(newIdentNode("FuturEx")).add(internalFutureType[1]).add(possibleExceptionsTuple) + internalFutureParameter = nnkIdentDefs.newTree(internalFutureSym, returnTypeWithException, newEmptyNode()) + prc.params[0] = returnTypeWithException var closureIterator = newProc(iteratorNameSym, [newIdentNode("FutureBase"), internalFutureParameter], procBody, nnkIteratorDef) closureIterator.pragma = newNimNode(nnkPragma, lineInfoFrom=prc.body) @@ -163,28 +180,10 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = # 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`.. - when defined(chronosStrictException): - closureIterator.addPragma(nnkExprColonExpr.newTree( - newIdentNode("raises"), - nnkBracket.newTree( - newIdentNode("Defect"), - newIdentNode("CatchableError") - ) - )) - else: - closureIterator.addPragma(nnkExprColonExpr.newTree( - newIdentNode("raises"), - nnkBracket.newTree( - newIdentNode("Defect"), - newIdentNode("CatchableError"), - newIdentNode("Exception") # Allow exception effects - ) - )) + closureIterator.addPragma(nnkExprColonExpr.newTree( + newIdentNode("raises"), + possibleExceptions + )) # If proc has an explicit gcsafe pragma, we add it to iterator as well. if prc.pragma.findChild(it.kind in {nnkSym, nnkIdent} and @@ -206,7 +205,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = outerProcBody.add( newVarStmt( retFutureSym, - newCall(newTree(nnkBracketExpr, ident "newFuture", subRetType), + newCall(newTree(nnkBracketExpr, ident "newFuturEx", subRetType, possibleExceptionsTuple), newLit(prcName)) ) ) @@ -229,19 +228,16 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = if prc.kind != nnkLambda: # TODO: Nim bug? prc.addPragma(newColonExpr(ident "stackTrace", ident "off")) + # The proc itself can't raise + prc.addPragma(nnkExprColonExpr.newTree( + newIdentNode("raises"), + nnkBracket.newTree())) + # See **Remark 435** in this file. # https://github.com/nim-lang/RFCs/issues/435 prc.addPragma(newIdentNode("gcsafe")) result = prc - if subtypeIsVoid: - # Add discardable pragma. - if returnType.kind == nnkEmpty: - # Add Future[void] - result.params[0] = - newNimNode(nnkBracketExpr, prc) - .add(newIdentNode("Future")) - .add(newIdentNode("void")) if procBody.kind != nnkEmpty: result.body = outerProcBody #echo(treeRepr(result)) @@ -278,6 +274,43 @@ template await*[T](f: Future[T]): untyped = else: unsupported "await is only available within {.async.}" +macro checkMagical(f, e: typed): untyped = + result = newNimNode(nnkStmtList) + + let types = getType(e) + if types.len < 1: return + for errorType in types[1..^1]: + result.add quote do: + if not isNil(`f`.error) and `f`.error of type `errorType`: + raise cast[ref `errorType`](`f`.error) + #echo treeRepr(result) + +template await*[T, E](f: FuturEx[T, E]): untyped = + when declared(chronosInternalRetFuture): + let chronosInternalTmpFuture: FuturEx[T, E] = f + chronosInternalRetFuture.child = chronosInternalTmpFuture + + # This "yield" is meant for a closure iterator in the caller. + yield chronosInternalTmpFuture + + # By the time we get control back here, we're guaranteed that the Future we + # just yielded has been completed (success, failure or cancellation), + # through a very complicated mechanism in which the caller proc (a regular + # closure) adds itself as a callback to chronosInternalTmpFuture. + # + # Callbacks are called only after completion and a copy of the closure + # iterator that calls this template is still in that callback's closure + # environment. That's where control actually gets back to us. + + chronosInternalRetFuture.child = nil + if chronosInternalRetFuture.mustCancel: + raise newCancelledError() + checkMagical(chronosInternalTmpFuture, E) + when T isnot void: + cast[type(f)](chronosInternalTmpFuture).internalRead() + else: + unsupported "await is only available within {.async.}" + template awaitne*[T](f: Future[T]): Future[T] = when declared(chronosInternalRetFuture): #work around https://github.com/nim-lang/Nim/issues/19193 From 45d7ba81e1a061c22e0f4058cbd6342c397a370f Mon Sep 17 00:00:00 2001 From: Tanguy Date: Mon, 3 Jan 2022 15:15:28 +0100 Subject: [PATCH 02/31] some fixes --- chronos/asyncfutures2.nim | 2 +- chronos/asyncmacro2.nim | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index 9c03390e2..af07fa685 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -57,7 +57,7 @@ type closure*: iterator(f: FuturEx[T, E]): FutureBase {.raises: [Defect, CatchableError, Exception], gcsafe.} value: T ## Stored value - Future*[T] = FuturEx[T, (CatchableError)] + Future*[T] = FuturEx[T, (CatchableError,)] FutureStr*[T] = ref object of Future[T] ## Future to hold GC strings diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index cde0a3bfd..606d041c7 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -84,17 +84,18 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = var possibleExceptions = nnkBracket.newTree(newIdentNode("CancelledError")) possibleExceptionsTuple = nnkTupleConstr.newTree(newIdentNode("CancelledError")) - foundRaises = false - for pragma in pragma(prc): + foundRaises = -1 + for index, pragma in pragma(prc): if pragma[0] == ident "raises": - foundRaises = true + foundRaises = index for possibleRaise in pragma[1]: possibleExceptions.add(possibleRaise) possibleExceptionsTuple.add(possibleRaise) break - if not foundRaises: + if foundRaises < 0: possibleExceptions.add(ident "CatchableError") possibleExceptionsTuple.add(ident "CatchableError") + else: pragma(prc).del(foundRaises) let returnType = prc.params[0] var baseType: NimNode From 896df6b3e977ad86ab61fba43744beb90f21e81d Mon Sep 17 00:00:00 2001 From: Tanguy Date: Mon, 3 Jan 2022 15:31:53 +0100 Subject: [PATCH 03/31] Nim 1.2 compat --- chronos/asyncmacro2.nim | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 606d041c7..b8dba81f6 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -85,6 +85,10 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = possibleExceptions = nnkBracket.newTree(newIdentNode("CancelledError")) possibleExceptionsTuple = nnkTupleConstr.newTree(newIdentNode("CancelledError")) foundRaises = -1 + + when (NimMajor, NimMinor) < (1, 4): + possibleExceptions.add(newIdentNode("Defect")) + for index, pragma in pragma(prc): if pragma[0] == ident "raises": foundRaises = index @@ -230,9 +234,14 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = prc.addPragma(newColonExpr(ident "stackTrace", ident "off")) # The proc itself can't raise + let emptyRaises = + when (NimMajor, NimMinor) < (1, 4): + nnkBracket.newTree(newIdentNode("Defect")) + else: + nnkBracket.newTree() prc.addPragma(nnkExprColonExpr.newTree( newIdentNode("raises"), - nnkBracket.newTree())) + emptyRaises)) # See **Remark 435** in this file. # https://github.com/nim-lang/RFCs/issues/435 From fde7649d372e013668d23b936686b8db9ec57348 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Mon, 3 Jan 2022 17:42:41 +0100 Subject: [PATCH 04/31] simpler things --- chronos/asyncfutures2.nim | 19 +++------------- chronos/asyncloop.nim | 4 ++-- chronos/asyncmacro2.nim | 46 +++++++++------------------------------ 3 files changed, 15 insertions(+), 54 deletions(-) diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index af07fa685..6029170c6 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -53,11 +53,11 @@ type # the future can be stored within the caller's stack frame. # How much refactoring is needed to make this a regular non-ref type? # Obviously, it will still be allocated on the heap when necessary. - FuturEx*[T, E] = ref object of FutureBase ## Typed future. - closure*: iterator(f: FuturEx[T, E]): FutureBase {.raises: [Defect, CatchableError, Exception], gcsafe.} + Future*[T] = ref object of FutureBase + closure*: iterator(f: Future[T]): FutureBase {.raises: [Defect, CatchableError, Exception], gcsafe.} value: T ## Stored value - Future*[T] = FuturEx[T, (CatchableError,)] + FuturEx*[T, E] = Future[T] FutureStr*[T] = ref object of Future[T] ## Future to hold GC strings @@ -108,9 +108,6 @@ template setupFutureBase(loc: ptr SrcLoc) = proc newFutureImpl[T](loc: ptr SrcLoc): Future[T] = setupFutureBase(loc) -proc newFuturExImpl[T, E](loc: ptr SrcLoc): FuturEx[T, E] = - setupFutureBase(loc) - proc newFutureSeqImpl[A, B](loc: ptr SrcLoc): FutureSeq[A, B] = setupFutureBase(loc) @@ -124,16 +121,6 @@ template newFuture*[T](fromProc: static[string] = ""): Future[T] = ## that this future belongs to, is a good habit as it helps with debugging. newFutureImpl[T](getSrcLocation(fromProc)) -template newFuturEx*[T, E](fromProc: static[string] = ""): FuturEx[T, E] = - ## Creates a new future. - ## - ## Specifying ``fromProc``, which is a string specifying the name of the proc - ## that this future belongs to, is a good habit as it helps with debugging. - newFuturExImpl[T, E](getSrcLocation(fromProc)) - -converter toFuture*[T, E](futurex: FuturEx[T, E]): Future[T] = - cast[Future[T]](futurex) - template newFutureSeq*[A, B](fromProc: static[string] = ""): FutureSeq[A, B] = ## Create a new future which can hold/preserve GC sequence until future will ## not be completed. diff --git a/chronos/asyncloop.nim b/chronos/asyncloop.nim index c550a2a5f..f731e73d0 100644 --- a/chronos/asyncloop.nim +++ b/chronos/asyncloop.nim @@ -868,10 +868,10 @@ proc callIdle*(cbproc: CallbackFunc) {.gcsafe, raises: [Defect].} = include asyncfutures2 -proc sleepAsync*(duration: Duration): FuturEx[void, (CancelledError,)] = +proc sleepAsync*(duration: Duration): Future[void] = ## Suspends the execution of the current async procedure for the next ## ``duration`` time. - var retFuture = newFuturEx[void, (CancelledError,)]("chronos.sleepAsync(Duration)") + var retFuture = newFuture[void]("chronos.sleepAsync(Duration)") let moment = Moment.fromNow(duration) var timer: TimerCallback diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index b8dba81f6..eae28319f 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -169,7 +169,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(newIdentNode("void")) else: returnType returnTypeWithException = newNimNode(nnkBracketExpr).add(newIdentNode("FuturEx")).add(internalFutureType[1]).add(possibleExceptionsTuple) - internalFutureParameter = nnkIdentDefs.newTree(internalFutureSym, returnTypeWithException, newEmptyNode()) + internalFutureParameter = nnkIdentDefs.newTree(internalFutureSym, internalFutureType, newEmptyNode()) prc.params[0] = returnTypeWithException var closureIterator = newProc(iteratorNameSym, [newIdentNode("FutureBase"), internalFutureParameter], procBody, nnkIteratorDef) @@ -210,7 +210,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = outerProcBody.add( newVarStmt( retFutureSym, - newCall(newTree(nnkBracketExpr, ident "newFuturEx", subRetType, possibleExceptionsTuple), + newCall(newTree(nnkBracketExpr, ident "newFuture", subRetType), newLit(prcName)) ) ) @@ -254,50 +254,24 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = #if prcName == "recvLineInto": # echo(toStrLit(result)) -template await*[T](f: Future[T]): untyped = - when declared(chronosInternalRetFuture): - #work around https://github.com/nim-lang/Nim/issues/19193 - when not declaredInScope(chronosInternalTmpFuture): - var chronosInternalTmpFuture {.inject.}: FutureBase = f - else: - chronosInternalTmpFuture = f - chronosInternalRetFuture.child = chronosInternalTmpFuture +macro checkMagical(f: typed): untyped = + if getTypeInst(f)[0].repr != "FuturEx": + return quote do: + `f`.internalCheckComplete() - # This "yield" is meant for a closure iterator in the caller. - yield chronosInternalTmpFuture - - # By the time we get control back here, we're guaranteed that the Future we - # just yielded has been completed (success, failure or cancellation), - # through a very complicated mechanism in which the caller proc (a regular - # closure) adds itself as a callback to chronosInternalTmpFuture. - # - # Callbacks are called only after completion and a copy of the closure - # iterator that calls this template is still in that callback's closure - # environment. That's where control actually gets back to us. - - chronosInternalRetFuture.child = nil - if chronosInternalRetFuture.mustCancel: - raise newCancelledError() - chronosInternalTmpFuture.internalCheckComplete() - when T isnot void: - cast[type(f)](chronosInternalTmpFuture).internalRead() - else: - unsupported "await is only available within {.async.}" - -macro checkMagical(f, e: typed): untyped = result = newNimNode(nnkStmtList) + let e = getTypeInst(f)[2] let types = getType(e) if types.len < 1: return for errorType in types[1..^1]: result.add quote do: if not isNil(`f`.error) and `f`.error of type `errorType`: raise cast[ref `errorType`](`f`.error) - #echo treeRepr(result) -template await*[T, E](f: FuturEx[T, E]): untyped = +template await*[T](f: Future[T]): untyped = when declared(chronosInternalRetFuture): - let chronosInternalTmpFuture: FuturEx[T, E] = f + let chronosInternalTmpFuture = f chronosInternalRetFuture.child = chronosInternalTmpFuture # This "yield" is meant for a closure iterator in the caller. @@ -315,7 +289,7 @@ template await*[T, E](f: FuturEx[T, E]): untyped = chronosInternalRetFuture.child = nil if chronosInternalRetFuture.mustCancel: raise newCancelledError() - checkMagical(chronosInternalTmpFuture, E) + checkMagical(chronosInternalTmpFuture) when T isnot void: cast[type(f)](chronosInternalTmpFuture).internalRead() else: From 8f765dfc7eaf9aaed88fb27fb6ecd83b4955760c Mon Sep 17 00:00:00 2001 From: Tanguy Date: Mon, 3 Jan 2022 18:49:52 +0100 Subject: [PATCH 05/31] Fixes for libp2p --- chronos/asyncmacro2.nim | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index eae28319f..3b815c7b0 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -90,7 +90,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = possibleExceptions.add(newIdentNode("Defect")) for index, pragma in pragma(prc): - if pragma[0] == ident "raises": + if pragma.kind == nnkExprColonExpr and pragma[0] == ident "raises": foundRaises = index for possibleRaise in pragma[1]: possibleExceptions.add(possibleRaise) @@ -122,6 +122,13 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = var outerProcBody = newNimNode(nnkStmtList, prc.body) + let + internalFutureType = + if subtypeIsVoid: + newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(newIdentNode("void")) + else: returnType + returnTypeWithException = newNimNode(nnkBracketExpr).add(newIdentNode("FuturEx")).add(internalFutureType[1]).add(possibleExceptionsTuple) + prc.params[0] = returnTypeWithException # -> iterator nameIter(chronosInternalRetFuture: Future[T]): FutureBase {.closure.} = # -> {.push warning[resultshadowed]: off.} @@ -163,14 +170,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = # -> complete(chronosInternalRetFuture) procBody.add(newCall(newIdentNode("complete"), internalFutureSym)) - let - internalFutureType = - if subtypeIsVoid: - newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(newIdentNode("void")) - else: returnType - returnTypeWithException = newNimNode(nnkBracketExpr).add(newIdentNode("FuturEx")).add(internalFutureType[1]).add(possibleExceptionsTuple) - internalFutureParameter = nnkIdentDefs.newTree(internalFutureSym, internalFutureType, newEmptyNode()) - prc.params[0] = returnTypeWithException + let internalFutureParameter = nnkIdentDefs.newTree(internalFutureSym, internalFutureType, newEmptyNode()) var closureIterator = newProc(iteratorNameSym, [newIdentNode("FutureBase"), internalFutureParameter], procBody, nnkIteratorDef) closureIterator.pragma = newNimNode(nnkPragma, lineInfoFrom=prc.body) From 3f53d013eb047b91849aa4a8d2f622e9d98d4d48 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Wed, 5 Jan 2022 16:39:20 +0100 Subject: [PATCH 06/31] Fixes for strictException --- chronos/asyncfutures2.nim | 9 +++++++-- chronos/asyncmacro2.nim | 5 +++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index 6029170c6..572ce05e2 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -53,10 +53,15 @@ type # the future can be stored within the caller's stack frame. # How much refactoring is needed to make this a regular non-ref type? # Obviously, it will still be allocated on the heap when necessary. - Future*[T] = ref object of FutureBase - closure*: iterator(f: Future[T]): FutureBase {.raises: [Defect, CatchableError, Exception], gcsafe.} + Future*[T] = ref object of FutureBase ## Typed future. + when defined(chronosStrictException): + closure*: iterator(f: Future[T]): FutureBase {.raises: [Defect, CatchableError], gcsafe.} + else: + closure*: iterator(f: Future[T]): FutureBase {.raises: [Defect, CatchableError, Exception], gcsafe.} value: T ## Stored value + # Future with a tuple of possible exception types + # eg FuturEx[void, (ValueError, OSError)] FuturEx*[T, E] = Future[T] FutureStr*[T] = ref object of Future[T] diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 3b815c7b0..43e429555 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -97,8 +97,9 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = possibleExceptionsTuple.add(possibleRaise) break if foundRaises < 0: - possibleExceptions.add(ident "CatchableError") - possibleExceptionsTuple.add(ident "CatchableError") + const defaultException = when defined(chronosStrictException): "CatchableError" else: "Exception" + possibleExceptions.add(ident defaultException) + possibleExceptionsTuple.add(ident defaultException) else: pragma(prc).del(foundRaises) let returnType = prc.params[0] From f8a55aeb4e356a67f0f7311a90f7b6467c505860 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Thu, 6 Jan 2022 10:32:26 +0100 Subject: [PATCH 07/31] better await exception check --- chronos/asyncmacro2.nim | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 43e429555..47b79650e 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -255,20 +255,44 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = #if prcName == "recvLineInto": # echo(toStrLit(result)) -macro checkMagical(f: typed): untyped = +macro checkFutureExceptions(f: typed): untyped = if getTypeInst(f)[0].repr != "FuturEx": return quote do: `f`.internalCheckComplete() - result = newNimNode(nnkStmtList) - + # For FuturEx[void, (ValueError, OSError), will do: + # if isNil(f.error): discard + # elif f.error of type ValueError: raise cast[ref ValueError](f.error) + # elif f.error of type OSError: raise cast[ref OSError](f.error) + # else: raiseAssert("Unhandled future exception: " & f.error.msg) + # + # In future nim versions, this could simply be + # {.cast(raises: [ValueError, OSError]).}: + # raise f.error let e = getTypeInst(f)[2] let types = getType(e) - if types.len < 1: return + + expectKind(types, nnkBracketExpr) + expectKind(types[0], nnkSym) + assert types[0].strVal == "tuple" + assert types.len > 1 + + result = nnkIfExpr.newTree( + nnkElifExpr.newTree( + quote do: isNil(`f`.error), + quote do: discard + ) + ) + for errorType in types[1..^1]: - result.add quote do: - if not isNil(`f`.error) and `f`.error of type `errorType`: - raise cast[ref `errorType`](`f`.error) + result.add nnkElifExpr.newTree( + quote do: `f`.error of type `errorType`, + quote do: raise cast[ref `errorType`](`f`.error) + ) + + result.add nnkElseExpr.newTree( + quote do: raiseAssert("Unhandled future exception: " & `f`.error.msg) + ) template await*[T](f: Future[T]): untyped = when declared(chronosInternalRetFuture): @@ -290,7 +314,7 @@ template await*[T](f: Future[T]): untyped = chronosInternalRetFuture.child = nil if chronosInternalRetFuture.mustCancel: raise newCancelledError() - checkMagical(chronosInternalTmpFuture) + checkFutureExceptions(chronosInternalTmpFuture) when T isnot void: cast[type(f)](chronosInternalTmpFuture).internalRead() else: From 1e8285bee14885b70dd77de28213055b8555f486 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Thu, 6 Jan 2022 13:11:04 +0100 Subject: [PATCH 08/31] Fix for template async proc --- chronos/asyncmacro2.nim | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 47b79650e..55b61df5b 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -127,6 +127,8 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = internalFutureType = if subtypeIsVoid: newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(newIdentNode("void")) + elif returnType.kind in nnkCallKinds and returnType[0].eqIdent("[]"): + newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(returnType[2]) else: returnType returnTypeWithException = newNimNode(nnkBracketExpr).add(newIdentNode("FuturEx")).add(internalFutureType[1]).add(possibleExceptionsTuple) prc.params[0] = returnTypeWithException From b9c229c9afc93be2076da5c7fad20385e35176a6 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Wed, 12 Jan 2022 12:14:11 +0100 Subject: [PATCH 09/31] make async work with procTy --- chronos/asyncmacro2.nim | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 55b61df5b..077c256a9 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -72,15 +72,20 @@ proc verifyReturnType(typeName: string) {.compileTime.} = macro unsupported(s: static[string]): untyped = error s +proc params2*(someProc: NimNode): NimNode = + if someProc.kind == nnkProcTy: + someProc[0] + else: + params(someProc) + + 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}: + if prc.kind notin {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo, nnkProcTy}: error("Cannot transform this node kind into an async proc." & " proc/method definition or lambda node expected.") - let prcName = prc.name.getName - var possibleExceptions = nnkBracket.newTree(newIdentNode("CancelledError")) possibleExceptionsTuple = nnkTupleConstr.newTree(newIdentNode("CancelledError")) @@ -102,7 +107,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = possibleExceptionsTuple.add(ident defaultException) else: pragma(prc).del(foundRaises) - let returnType = prc.params[0] + let returnType = prc.params2[0] var baseType: NimNode # Verify that the return type is a Future[T] if returnType.kind == nnkBracketExpr: @@ -121,7 +126,7 @@ 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) + var outerProcBody = newNimNode(nnkStmtList, prc) let internalFutureType = @@ -131,7 +136,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(returnType[2]) else: returnType returnTypeWithException = newNimNode(nnkBracketExpr).add(newIdentNode("FuturEx")).add(internalFutureType[1]).add(possibleExceptionsTuple) - prc.params[0] = returnTypeWithException + prc.params2[0] = returnTypeWithException # -> iterator nameIter(chronosInternalRetFuture: Future[T]): FutureBase {.closure.} = # -> {.push warning[resultshadowed]: off.} @@ -140,10 +145,13 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = # -> # -> complete(chronosInternalRetFuture, result) let internalFutureSym = ident "chronosInternalRetFuture" - var iteratorNameSym = genSym(nskIterator, $prcName) - var procBody = prc.body.processBody(internalFutureSym, subtypeIsVoid) + var procBody = + if prc.kind == nnkProcTy: newNimNode(nnkEmpty) + else: prc.body.processBody(internalFutureSym, subtypeIsVoid) # don't do anything with forward bodies (empty) if procBody.kind != nnkEmpty: + let prcName = prc.name.getName + var iteratorNameSym = genSym(nskIterator, $prcName) if subtypeIsVoid: let resultTemplate = quote do: template result: auto {.used.} = @@ -233,7 +241,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = # -> return resultFuture outerProcBody.add newNimNode(nnkReturnStmt, prc.body[^1]).add(retFutureSym) - if prc.kind != nnkLambda: # TODO: Nim bug? + if prc.kind != nnkLambda and prc.kind != nnkProcTy: # TODO: Nim bug? prc.addPragma(newColonExpr(ident "stackTrace", ident "off")) # The proc itself can't raise From 6589cd28d96c266befbf64be163a4b58e96453cb Mon Sep 17 00:00:00 2001 From: Tanguy Date: Sat, 15 Jan 2022 14:20:50 +0100 Subject: [PATCH 10/31] FuturEx is now a ref object type --- chronos/asyncfutures2.nim | 12 +++++++++++- chronos/asyncmacro2.nim | 31 ++++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index 572ce05e2..2ed29b110 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -62,7 +62,7 @@ type # Future with a tuple of possible exception types # eg FuturEx[void, (ValueError, OSError)] - FuturEx*[T, E] = Future[T] + FuturEx*[T, E] = ref object of Future[T] FutureStr*[T] = ref object of Future[T] ## Future to hold GC strings @@ -113,6 +113,9 @@ template setupFutureBase(loc: ptr SrcLoc) = proc newFutureImpl[T](loc: ptr SrcLoc): Future[T] = setupFutureBase(loc) +proc newFuturExImpl[T, E](loc: ptr SrcLoc): FuturEx[T, E] = + setupFutureBase(loc) + proc newFutureSeqImpl[A, B](loc: ptr SrcLoc): FutureSeq[A, B] = setupFutureBase(loc) @@ -126,6 +129,13 @@ template newFuture*[T](fromProc: static[string] = ""): Future[T] = ## that this future belongs to, is a good habit as it helps with debugging. newFutureImpl[T](getSrcLocation(fromProc)) +template newFuturEx*[T, E](fromProc: static[string] = ""): FuturEx[T, E] = + ## Creates a new future. + ## + ## Specifying ``fromProc``, which is a string specifying the name of the proc + ## that this future belongs to, is a good habit as it helps with debugging. + newFuturExImpl[T, E](getSrcLocation(fromProc)) + template newFutureSeq*[A, B](fromProc: static[string] = ""): FutureSeq[A, B] = ## Create a new future which can hold/preserve GC sequence until future will ## not be completed. diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 077c256a9..4c869e77d 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -136,7 +136,16 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(returnType[2]) else: returnType returnTypeWithException = newNimNode(nnkBracketExpr).add(newIdentNode("FuturEx")).add(internalFutureType[1]).add(possibleExceptionsTuple) - prc.params2[0] = returnTypeWithException + + # Rewrite return type + if foundRaises >= 0: + prc.params2[0] = nnkBracketExpr.newTree( + newIdentNode("FuturEx"), + internalFutureType[1], + possibleExceptionsTuple + ) + elif subtypeIsVoid: + prc.params2[0] = internalFutureType # -> iterator nameIter(chronosInternalRetFuture: Future[T]): FutureBase {.closure.} = # -> {.push warning[resultshadowed]: off.} @@ -207,7 +216,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = closureIterator.addPragma(newIdentNode("gcsafe")) outerProcBody.add(closureIterator) - # -> var resultFuture = newFuture[T]() + # -> var resultFuture = newFuturEx[T ,E]() # declared at the end to be sure that the closure # doesn't reference it, avoid cyclic ref (#203) var retFutureSym = ident "resultFuture" @@ -221,7 +230,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = outerProcBody.add( newVarStmt( retFutureSym, - newCall(newTree(nnkBracketExpr, ident "newFuture", subRetType), + newCall(newTree(nnkBracketExpr, ident "newFuturEx", subRetType, possibleExceptionsTuple), newLit(prcName)) ) ) @@ -266,10 +275,6 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = # echo(toStrLit(result)) macro checkFutureExceptions(f: typed): untyped = - if getTypeInst(f)[0].repr != "FuturEx": - return quote do: - `f`.internalCheckComplete() - # For FuturEx[void, (ValueError, OSError), will do: # if isNil(f.error): discard # elif f.error of type ValueError: raise cast[ref ValueError](f.error) @@ -306,7 +311,12 @@ macro checkFutureExceptions(f: typed): untyped = template await*[T](f: Future[T]): untyped = when declared(chronosInternalRetFuture): - let chronosInternalTmpFuture = f + #work around https://github.com/nim-lang/Nim/issues/19193 + when not declaredInScope(chronosInternalTmpFuture): + var chronosInternalTmpFuture {.inject.}: FutureBase = f + else: + chronosInternalTmpFuture = f + chronosInternalRetFuture.child = chronosInternalTmpFuture # This "yield" is meant for a closure iterator in the caller. @@ -324,7 +334,10 @@ template await*[T](f: Future[T]): untyped = chronosInternalRetFuture.child = nil if chronosInternalRetFuture.mustCancel: raise newCancelledError() - checkFutureExceptions(chronosInternalTmpFuture) + when f is FuturEx: + checkFutureExceptions(chronosInternalTmpFuture) + else: + chronosInternalTmpFuture.internalCheckComplete() when T isnot void: cast[type(f)](chronosInternalTmpFuture).internalRead() else: From 8c36af85e3ac8b555ac462404b9284612da7db4f Mon Sep 17 00:00:00 2001 From: Tanguy Date: Sat, 15 Jan 2022 14:53:25 +0100 Subject: [PATCH 11/31] add tests --- chronos/asyncmacro2.nim | 10 +++++---- tests/testmacro.nim | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 4c869e77d..e30dfa772 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -274,7 +274,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = #if prcName == "recvLineInto": # echo(toStrLit(result)) -macro checkFutureExceptions(f: typed): untyped = +macro checkFutureExceptions(f, typ: typed): untyped = # For FuturEx[void, (ValueError, OSError), will do: # if isNil(f.error): discard # elif f.error of type ValueError: raise cast[ref ValueError](f.error) @@ -284,7 +284,7 @@ macro checkFutureExceptions(f: typed): untyped = # In future nim versions, this could simply be # {.cast(raises: [ValueError, OSError]).}: # raise f.error - let e = getTypeInst(f)[2] + let e = getTypeInst(typ)[2] let types = getType(e) expectKind(types, nnkBracketExpr) @@ -302,7 +302,9 @@ macro checkFutureExceptions(f: typed): untyped = for errorType in types[1..^1]: result.add nnkElifExpr.newTree( quote do: `f`.error of type `errorType`, - quote do: raise cast[ref `errorType`](`f`.error) + nnkRaiseStmt.newNimNode(lineInfoFrom=typ).add( + quote do: cast[ref `errorType`](`f`.error) + ) ) result.add nnkElseExpr.newTree( @@ -335,7 +337,7 @@ template await*[T](f: Future[T]): untyped = if chronosInternalRetFuture.mustCancel: raise newCancelledError() when f is FuturEx: - checkFutureExceptions(chronosInternalTmpFuture) + checkFutureExceptions(chronosInternalTmpFuture, f) else: chronosInternalTmpFuture.internalCheckComplete() when T isnot void: diff --git a/tests/testmacro.nim b/tests/testmacro.nim index c18b37ee9..1af93aec4 100644 --- a/tests/testmacro.nim +++ b/tests/testmacro.nim @@ -80,3 +80,48 @@ suite "Macro transformations test suite": check waitFor(testAwait()) == true test "`awaitne` command test": check waitFor(testAwaitne()) == true + +suite "Exceptions tracking": + test "Can raise valid exception": + proc test1 {.async.} = raise newException(ValueError, "hey") + proc test2 {.async, raises: [ValueError].} = raise newException(ValueError, "hey") + proc test3 {.async, raises: [IOError, ValueError].} = + if 1 == 2: + raise newException(ValueError, "hey") + else: + raise newException(IOError, "hey") + + proc test4 {.async, raises: [].} = raise newException(Defect, "hey") + + test "Cannot raise invalid exception": + check not (compiles do: + proc test3 {.async, raises: [IOError].} = raise newException(ValueError, "hey") + ) + + test "Non-raising compatibility": + proc test1 {.async, raises: [ValueError].} = raise newException(ValueError, "hey") + let testVar: Future[void] = test1() + + proc test2 {.async.} = raise newException(ValueError, "hey") + let testVar2: proc: Future[void] = test2 + + # Doesn't work unfortunately + #let testVar3: proc: Future[void] = test1 + + test "Cannot store invalid future types": + proc test1 {.async, raises: [ValueError].} = raise newException(ValueError, "hey") + proc test2 {.async, raises: [IOError].} = raise newException(IOError, "hey") + + var a = test1() + check not compiles(a = test2()) + + test "Await raises the correct types": + proc test1 {.async, raises: [ValueError].} = raise newException(ValueError, "hey") + proc test2 {.async, raises: [ValueError].} = await test1() + check not (compiles do: + proc test3 {.async, raises: [].} = await test1() + ) + + test "Can create callbacks": + proc test1 {.async, raises: [ValueError].} = raise newException(ValueError, "hey") + let callback: proc {.async, raises: [ValueError].} = test1 From c6075f86cf30d24003fc7241667c1febd0d5f1a9 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Mon, 17 Jan 2022 12:25:04 +0100 Subject: [PATCH 12/31] update test --- tests/testfut.nim | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/testfut.nim b/tests/testfut.nim index 656cacd69..25315dc4e 100644 --- a/tests/testfut.nim +++ b/tests/testfut.nim @@ -1065,20 +1065,23 @@ suite "Future[T] behavior test suite": let loc31 = fut3.location[1] proc chk(loc: ptr SrcLoc, file: string, line: int, - procedure: string): bool = + procedure: string) = if len(procedure) == 0: - (loc.line == line) and ($loc.file == file) + check: + loc.line == line + $loc.file == file else: - (loc.line == line) and ($loc.file == file) and - (loc.procedure == procedure) - - check: - chk(loc10, "testfut.nim", 1041, "macroFuture") - chk(loc11, "testfut.nim", 1042, "") - chk(loc20, "testfut.nim", 1054, "template") - chk(loc21, "testfut.nim", 1057, "") - chk(loc30, "testfut.nim", 1051, "procedure") - chk(loc31, "testfut.nim", 1058, "") + check: + loc.line == line + $loc.file == file + loc.procedure == procedure + + chk(loc10, "testfut.nim", 1041, "macroFuture") + chk(loc11, "testfut.nim", 1041, "") + chk(loc20, "testfut.nim", 1054, "template") + chk(loc21, "testfut.nim", 1057, "") + chk(loc30, "testfut.nim", 1051, "procedure") + chk(loc31, "testfut.nim", 1058, "") test "withTimeout(fut) should wait cancellation test": proc futureNeverEnds(): Future[void] = From 69fcf74630b28bd6c67d9bc969932977e20b34d1 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Mon, 17 Jan 2022 12:50:39 +0100 Subject: [PATCH 13/31] update readme --- README.md | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 328801832..7bbbe300e 100644 --- a/README.md +++ b/README.md @@ -151,9 +151,9 @@ feet, in a certain section, is to not use `await` in it. ### Error handling -Exceptions inheriting from `CatchableError` are caught by hidden `try` blocks -and placed in the `Future.error` field, changing the future's status to -`Failed`. +Exceptions inheriting from `Exception` (or `CatchableError` when +`-d:chronosStrictException` is enabled) are caught by hidden `try` blocks +and placed in the `Future.error` field, changing the future status to `Failed`. When a future is awaited, that exception is re-raised, only to be caught again by a hidden `try` block in the calling async procedure. That's how these @@ -211,6 +211,43 @@ originating from tasks on the dispatcher queue. It is however possible that `Defect` that happen in tasks bubble up through `poll` as these are not caught by the transformation. +#### Checked exceptions + +By specifying a `raises` list to an async procedure, you can check which +exceptions can be thrown by it. +```nim +proc p1(): Future[void] {.async, raises: [IOError].} = + assert not (compiles do: raise newException(ValueError, "uh-uh")) + raise newException(IOError, "works") # Or any child of IOError +``` + +Note that this won't work with a pushed `raises` pragma. Under the hood, +the return type of `p1` will be rewritten to another type, which will +convey raises informations to await. + +```nim +proc p2(): Future[void] {.async, raises: [IOError].} = + await p1() # Works, because await knows that p1 + # can only raise IOError +``` + +The hidden type (`FuturEx`) is implicitely convertible into a Future. +However, it may causes issues when creating callback or methods +```nim +proc p3(): Future[void] {.async, raises: [IOError].} = + let fut: Future[void] = p1() # works + assert not compiles(await fut) # await lost informations about raises, + # so it can raise anything + + # Callbacks + assert not(compiles do: let cb1: proc(): Future[void] = p1) # doesn't work + let cb2: proc(): Future[void] {.async, raises: [IOError].} = p1 # works + assert not(compiles do: + type c = proc(): Future[void] {.async, raises: [IOError, ValueError].} + let cb3: c = p1 # doesn't work, the raises must match _exactly_ + ) +``` + ### Platform independence Several functions in `chronos` are backed by the operating system, such as From 0f496dbd62bc83f436063b751eb14775135e5344 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Thu, 24 Feb 2022 15:26:38 +0100 Subject: [PATCH 14/31] Switch to asyncraises pragma --- README.md | 19 +++++---- chronos/asyncmacro2.nim | 87 ++++++++++++++++++++++++++++------------- tests/testmacro.nim | 25 ++++++------ 3 files changed, 82 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 7bbbe300e..613ac3520 100644 --- a/README.md +++ b/README.md @@ -213,20 +213,19 @@ by the transformation. #### Checked exceptions -By specifying a `raises` list to an async procedure, you can check which -exceptions can be thrown by it. +By using `asyncraises` instead of `async`, you can check which +exceptions can be thrown by an async procedure. ```nim -proc p1(): Future[void] {.async, raises: [IOError].} = +proc p1(): Future[void] {.asyncraises: [IOError].} = assert not (compiles do: raise newException(ValueError, "uh-uh")) raise newException(IOError, "works") # Or any child of IOError ``` -Note that this won't work with a pushed `raises` pragma. Under the hood, -the return type of `p1` will be rewritten to another type, which will -convey raises informations to await. +Under the hood, the return type of `p1` will be rewritten to another type, +which will convey raises informations to await. ```nim -proc p2(): Future[void] {.async, raises: [IOError].} = +proc p2(): Future[void] {.asyncraises: [IOError].} = await p1() # Works, because await knows that p1 # can only raise IOError ``` @@ -234,16 +233,16 @@ proc p2(): Future[void] {.async, raises: [IOError].} = The hidden type (`FuturEx`) is implicitely convertible into a Future. However, it may causes issues when creating callback or methods ```nim -proc p3(): Future[void] {.async, raises: [IOError].} = +proc p3(): Future[void] {.asyncraises: [IOError].} = let fut: Future[void] = p1() # works assert not compiles(await fut) # await lost informations about raises, # so it can raise anything # Callbacks assert not(compiles do: let cb1: proc(): Future[void] = p1) # doesn't work - let cb2: proc(): Future[void] {.async, raises: [IOError].} = p1 # works + let cb2: proc(): Future[void] {.asyncraises: [IOError].} = p1 # works assert not(compiles do: - type c = proc(): Future[void] {.async, raises: [IOError, ValueError].} + type c = proc(): Future[void] {.asyncraises: [IOError, ValueError].} let cb3: c = p1 # doesn't work, the raises must match _exactly_ ) ``` diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index e30dfa772..666aa159f 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -79,7 +79,7 @@ proc params2*(someProc: NimNode): NimNode = params(someProc) -proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = +proc asyncSingleProc(prc, raises: NimNode, trackExceptions: bool): 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, nnkProcTy}: @@ -87,25 +87,22 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = " proc/method definition or lambda node expected.") var - possibleExceptions = nnkBracket.newTree(newIdentNode("CancelledError")) - possibleExceptionsTuple = nnkTupleConstr.newTree(newIdentNode("CancelledError")) + raisesTuple = + if raises.len > 0: + nnkTupleConstr.newTree() + else: + ident("void") foundRaises = -1 - when (NimMajor, NimMinor) < (1, 4): - possibleExceptions.add(newIdentNode("Defect")) - for index, pragma in pragma(prc): if pragma.kind == nnkExprColonExpr and pragma[0] == ident "raises": + warning("The raises pragma doesn't work on async procedure. " & + "Use asyncraises instead") foundRaises = index - for possibleRaise in pragma[1]: - possibleExceptions.add(possibleRaise) - possibleExceptionsTuple.add(possibleRaise) - break - if foundRaises < 0: - const defaultException = when defined(chronosStrictException): "CatchableError" else: "Exception" - possibleExceptions.add(ident defaultException) - possibleExceptionsTuple.add(ident defaultException) - else: pragma(prc).del(foundRaises) + if foundRaises >= 0: pragma(prc).del(foundRaises) + + for possibleRaise in raises: + raisesTuple.add(possibleRaise) let returnType = prc.params2[0] var baseType: NimNode @@ -124,7 +121,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = verifyReturnType(repr(returnType)) 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) @@ -135,14 +132,14 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = elif returnType.kind in nnkCallKinds and returnType[0].eqIdent("[]"): newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(returnType[2]) else: returnType - returnTypeWithException = newNimNode(nnkBracketExpr).add(newIdentNode("FuturEx")).add(internalFutureType[1]).add(possibleExceptionsTuple) + returnTypeWithException = newNimNode(nnkBracketExpr).add(newIdentNode("FuturEx")).add(internalFutureType[1]).add(raisesTuple) - # Rewrite return type - if foundRaises >= 0: + #Rewrite return type + if trackExceptions: prc.params2[0] = nnkBracketExpr.newTree( newIdentNode("FuturEx"), internalFutureType[1], - possibleExceptionsTuple + raisesTuple ) elif subtypeIsVoid: prc.params2[0] = internalFutureType @@ -205,9 +202,14 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = # for more details. closureIterator.addPragma(newIdentNode("gcsafe")) + let closureRaises = raises.copy() + closureRaises.add(ident("CancelledError")) + when (NimMajor, NimMinor) < (1, 4): + closureRaises.add(ident("Defect")) + closureIterator.addPragma(nnkExprColonExpr.newTree( newIdentNode("raises"), - possibleExceptions + closureRaises )) # If proc has an explicit gcsafe pragma, we add it to iterator as well. @@ -230,7 +232,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = outerProcBody.add( newVarStmt( retFutureSym, - newCall(newTree(nnkBracketExpr, ident "newFuturEx", subRetType, possibleExceptionsTuple), + newCall(newTree(nnkBracketExpr, ident "newFuturEx", subRetType, raisesTuple), newLit(prcName)) ) ) @@ -277,6 +279,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = macro checkFutureExceptions(f, typ: typed): untyped = # For FuturEx[void, (ValueError, OSError), will do: # if isNil(f.error): discard + # elif f.error of type CancelledError: raise cast[ref CancelledError](f.error) # elif f.error of type ValueError: raise cast[ref ValueError](f.error) # elif f.error of type OSError: raise cast[ref OSError](f.error) # else: raiseAssert("Unhandled future exception: " & f.error.msg) @@ -287,6 +290,14 @@ macro checkFutureExceptions(f, typ: typed): untyped = let e = getTypeInst(typ)[2] let types = getType(e) + if types.eqIdent("void"): + return quote do: + if not(isNil(`f`.error)): + if `f`.error of type CancelledError: + raise cast[ref CancelledError](`f`.error) + else: + raiseAssert("Unhandled future exception: " & `f`.error.msg) + expectKind(types, nnkBracketExpr) expectKind(types[0], nnkSym) assert types[0].strVal == "tuple" @@ -299,6 +310,12 @@ macro checkFutureExceptions(f, typ: typed): untyped = ) ) + result.add nnkElifExpr.newTree( + quote do: `f`.error of type CancelledError, + nnkRaiseStmt.newNimNode(lineInfoFrom=typ).add( + quote do: cast[ref CancelledError](`f`.error) + ) + ) for errorType in types[1..^1]: result.add nnkElifExpr.newTree( quote do: `f`.error of type `errorType`, @@ -361,14 +378,30 @@ template awaitne*[T](f: Future[T]): Future[T] = else: unsupported "awaitne is only available within {.async.}" -macro async*(prc: untyped): untyped = - ## Macro which processes async procedures into the appropriate - ## iterators and yield statements. +proc asyncMultipleProcs( + prc, raises: NimNode, + trackExceptions: bool): NimNode {.compiletime.} = if prc.kind == nnkStmtList: for oneProc in prc: result = newStmtList() - result.add asyncSingleProc(oneProc) + result.add asyncSingleProc(oneProc, raises, trackExceptions) else: - result = asyncSingleProc(prc) + result = asyncSingleProc(prc, raises, trackExceptions) when defined(nimDumpAsync): echo repr result + +macro async*(prc: untyped): untyped = + ## Macro which processes async procedures into the appropriate + ## iterators and yield statements. + + const defaultException = + when defined(chronosStrictException): "CatchableError" + else: "Exception" + let possibleExceptions = nnkBracket.newTree(newIdentNode(defaultException)) + asyncMultipleProcs(prc, possibleExceptions, false) + +macro asyncraises*(possibleExceptions, prc: untyped): untyped = + asyncMultipleProcs(prc, possibleExceptions, true) + +template asyncraises*(prc: untyped): untyped = + {.error: "Use .asyncraises: [].".} diff --git a/tests/testmacro.nim b/tests/testmacro.nim index 1af93aec4..8f92be637 100644 --- a/tests/testmacro.nim +++ b/tests/testmacro.nim @@ -84,22 +84,23 @@ suite "Macro transformations test suite": suite "Exceptions tracking": test "Can raise valid exception": proc test1 {.async.} = raise newException(ValueError, "hey") - proc test2 {.async, raises: [ValueError].} = raise newException(ValueError, "hey") - proc test3 {.async, raises: [IOError, ValueError].} = + proc test2 {.asyncraises: [ValueError].} = raise newException(ValueError, "hey") + proc test3 {.asyncraises: [IOError, ValueError].} = if 1 == 2: raise newException(ValueError, "hey") else: raise newException(IOError, "hey") - proc test4 {.async, raises: [].} = raise newException(Defect, "hey") + proc test4 {.asyncraises: [].} = raise newException(Defect, "hey") + proc test5 {.asyncraises: [].} = await test5() test "Cannot raise invalid exception": check not (compiles do: - proc test3 {.async, raises: [IOError].} = raise newException(ValueError, "hey") + proc test3 {.asyncraises: [IOError].} = raise newException(ValueError, "hey") ) test "Non-raising compatibility": - proc test1 {.async, raises: [ValueError].} = raise newException(ValueError, "hey") + proc test1 {.asyncraises: [ValueError].} = raise newException(ValueError, "hey") let testVar: Future[void] = test1() proc test2 {.async.} = raise newException(ValueError, "hey") @@ -109,19 +110,19 @@ suite "Exceptions tracking": #let testVar3: proc: Future[void] = test1 test "Cannot store invalid future types": - proc test1 {.async, raises: [ValueError].} = raise newException(ValueError, "hey") - proc test2 {.async, raises: [IOError].} = raise newException(IOError, "hey") + proc test1 {.asyncraises: [ValueError].} = raise newException(ValueError, "hey") + proc test2 {.asyncraises: [IOError].} = raise newException(IOError, "hey") var a = test1() check not compiles(a = test2()) test "Await raises the correct types": - proc test1 {.async, raises: [ValueError].} = raise newException(ValueError, "hey") - proc test2 {.async, raises: [ValueError].} = await test1() + proc test1 {.asyncraises: [ValueError].} = raise newException(ValueError, "hey") + proc test2 {.asyncraises: [ValueError].} = await test1() check not (compiles do: - proc test3 {.async, raises: [].} = await test1() + proc test3 {.asyncraises: [].} = await test1() ) test "Can create callbacks": - proc test1 {.async, raises: [ValueError].} = raise newException(ValueError, "hey") - let callback: proc {.async, raises: [ValueError].} = test1 + proc test1 {.asyncraises: [ValueError].} = raise newException(ValueError, "hey") + let callback: proc {.asyncraises: [ValueError].} = test1 From 79fa1ca12ce86946b1d4c8fac219a7a128850ab8 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Thu, 24 Feb 2022 15:36:17 +0100 Subject: [PATCH 15/31] Address tests review comments --- tests/testmacro.nim | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/tests/testmacro.nim b/tests/testmacro.nim index 8f92be637..ab5de0da3 100644 --- a/tests/testmacro.nim +++ b/tests/testmacro.nim @@ -82,6 +82,8 @@ suite "Macro transformations test suite": check waitFor(testAwaitne()) == true suite "Exceptions tracking": + template checkNotCompiles(body: untyped) = + check (not compiles(body)) test "Can raise valid exception": proc test1 {.async.} = raise newException(ValueError, "hey") proc test2 {.asyncraises: [ValueError].} = raise newException(ValueError, "hey") @@ -95,9 +97,8 @@ suite "Exceptions tracking": proc test5 {.asyncraises: [].} = await test5() test "Cannot raise invalid exception": - check not (compiles do: + checkNotCompiles: proc test3 {.asyncraises: [IOError].} = raise newException(ValueError, "hey") - ) test "Non-raising compatibility": proc test1 {.asyncraises: [ValueError].} = raise newException(ValueError, "hey") @@ -114,15 +115,27 @@ suite "Exceptions tracking": proc test2 {.asyncraises: [IOError].} = raise newException(IOError, "hey") var a = test1() - check not compiles(a = test2()) + checkNotCompiles: + a = test2() test "Await raises the correct types": proc test1 {.asyncraises: [ValueError].} = raise newException(ValueError, "hey") proc test2 {.asyncraises: [ValueError].} = await test1() - check not (compiles do: + checkNotCompiles: proc test3 {.asyncraises: [].} = await test1() - ) test "Can create callbacks": proc test1 {.asyncraises: [ValueError].} = raise newException(ValueError, "hey") let callback: proc {.asyncraises: [ValueError].} = test1 + + test "Can return values": + proc test1: Future[int] {.asyncraises: [ValueError].} = + if 1 == 0: raise newException(ValueError, "hey") + return 12 + proc test2: Future[int] {.asyncraises: [ValueError, IOError].} = + return await test1() + + checkNotCompiles: + proc test3: Future[int] {.asyncraises: [].}= await test1() + + check waitFor(test2()) == 12 From e12147ce0a293ac605ac9b4595863d990f617973 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Thu, 24 Feb 2022 15:41:11 +0100 Subject: [PATCH 16/31] Rename FuturEx to RaiseTrackingFuture --- README.md | 2 +- chronos/asyncfutures2.nim | 10 +++++----- chronos/asyncmacro2.nim | 16 ++++++++++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 613ac3520..ad5ced8ac 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ proc p2(): Future[void] {.asyncraises: [IOError].} = # can only raise IOError ``` -The hidden type (`FuturEx`) is implicitely convertible into a Future. +The hidden type (`RaiseTrackingFuture`) is implicitely convertible into a Future. However, it may causes issues when creating callback or methods ```nim proc p3(): Future[void] {.asyncraises: [IOError].} = diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index 2ed29b110..06d0478ff 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -61,8 +61,8 @@ type value: T ## Stored value # Future with a tuple of possible exception types - # eg FuturEx[void, (ValueError, OSError)] - FuturEx*[T, E] = ref object of Future[T] + # eg RaiseTrackingFuture[void, (ValueError, OSError)] + RaiseTrackingFuture*[T, E] = ref object of Future[T] FutureStr*[T] = ref object of Future[T] ## Future to hold GC strings @@ -113,7 +113,7 @@ template setupFutureBase(loc: ptr SrcLoc) = proc newFutureImpl[T](loc: ptr SrcLoc): Future[T] = setupFutureBase(loc) -proc newFuturExImpl[T, E](loc: ptr SrcLoc): FuturEx[T, E] = +proc newRaiseTrackingFutureImpl[T, E](loc: ptr SrcLoc): RaiseTrackingFuture[T, E] = setupFutureBase(loc) proc newFutureSeqImpl[A, B](loc: ptr SrcLoc): FutureSeq[A, B] = @@ -129,12 +129,12 @@ template newFuture*[T](fromProc: static[string] = ""): Future[T] = ## that this future belongs to, is a good habit as it helps with debugging. newFutureImpl[T](getSrcLocation(fromProc)) -template newFuturEx*[T, E](fromProc: static[string] = ""): FuturEx[T, E] = +template newRaiseTrackingFuture*[T, E](fromProc: static[string] = ""): RaiseTrackingFuture[T, E] = ## Creates a new future. ## ## Specifying ``fromProc``, which is a string specifying the name of the proc ## that this future belongs to, is a good habit as it helps with debugging. - newFuturExImpl[T, E](getSrcLocation(fromProc)) + newRaiseTrackingFutureImpl[T, E](getSrcLocation(fromProc)) template newFutureSeq*[A, B](fromProc: static[string] = ""): FutureSeq[A, B] = ## Create a new future which can hold/preserve GC sequence until future will diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 666aa159f..d34cee312 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -132,12 +132,16 @@ proc asyncSingleProc(prc, raises: NimNode, trackExceptions: bool): NimNode {.com elif returnType.kind in nnkCallKinds and returnType[0].eqIdent("[]"): newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(returnType[2]) else: returnType - returnTypeWithException = newNimNode(nnkBracketExpr).add(newIdentNode("FuturEx")).add(internalFutureType[1]).add(raisesTuple) + returnTypeWithException = + newNimNode(nnkBracketExpr). + add(newIdentNode("RaiseTrackingFuture")). + add(internalFutureType[1]). + add(raisesTuple) #Rewrite return type if trackExceptions: prc.params2[0] = nnkBracketExpr.newTree( - newIdentNode("FuturEx"), + newIdentNode("RaiseTrackingFuture"), internalFutureType[1], raisesTuple ) @@ -218,7 +222,7 @@ proc asyncSingleProc(prc, raises: NimNode, trackExceptions: bool): NimNode {.com closureIterator.addPragma(newIdentNode("gcsafe")) outerProcBody.add(closureIterator) - # -> var resultFuture = newFuturEx[T ,E]() + # -> var resultFuture = newRaiseTrackingFuture[T ,E]() # declared at the end to be sure that the closure # doesn't reference it, avoid cyclic ref (#203) var retFutureSym = ident "resultFuture" @@ -232,7 +236,7 @@ proc asyncSingleProc(prc, raises: NimNode, trackExceptions: bool): NimNode {.com outerProcBody.add( newVarStmt( retFutureSym, - newCall(newTree(nnkBracketExpr, ident "newFuturEx", subRetType, raisesTuple), + newCall(newTree(nnkBracketExpr, ident "newRaiseTrackingFuture", subRetType, raisesTuple), newLit(prcName)) ) ) @@ -277,7 +281,7 @@ proc asyncSingleProc(prc, raises: NimNode, trackExceptions: bool): NimNode {.com # echo(toStrLit(result)) macro checkFutureExceptions(f, typ: typed): untyped = - # For FuturEx[void, (ValueError, OSError), will do: + # For RaiseTrackingFuture[void, (ValueError, OSError), will do: # if isNil(f.error): discard # elif f.error of type CancelledError: raise cast[ref CancelledError](f.error) # elif f.error of type ValueError: raise cast[ref ValueError](f.error) @@ -353,7 +357,7 @@ template await*[T](f: Future[T]): untyped = chronosInternalRetFuture.child = nil if chronosInternalRetFuture.mustCancel: raise newCancelledError() - when f is FuturEx: + when f is RaiseTrackingFuture: checkFutureExceptions(chronosInternalTmpFuture, f) else: chronosInternalTmpFuture.internalCheckComplete() From e6faffdc7d9358f420773cb8ee3b8d9449909b6f Mon Sep 17 00:00:00 2001 From: Tanguy Date: Thu, 24 Feb 2022 15:48:02 +0100 Subject: [PATCH 17/31] Fix typo --- chronos/asyncmacro2.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index d34cee312..9fb0d59e7 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -384,7 +384,7 @@ template awaitne*[T](f: Future[T]): Future[T] = proc asyncMultipleProcs( prc, raises: NimNode, - trackExceptions: bool): NimNode {.compiletime.} = + trackExceptions: bool): NimNode {.compileTime.} = if prc.kind == nnkStmtList: for oneProc in prc: result = newStmtList() From 673d03febe359f6efc88bac20d963480a7e5b195 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Fri, 25 Feb 2022 12:41:32 +0100 Subject: [PATCH 18/31] Split asyncraises into async, asyncraises --- README.md | 14 +++++----- chronos/asyncmacro2.nim | 57 +++++++++++++++++++---------------------- tests/testmacro.nim | 32 +++++++++++------------ 3 files changed, 49 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index ad5ced8ac..9f48e1a6b 100644 --- a/README.md +++ b/README.md @@ -213,10 +213,10 @@ by the transformation. #### Checked exceptions -By using `asyncraises` instead of `async`, you can check which -exceptions can be thrown by an async procedure. +By specifying a `asyncraises` list to an async procedure, you can check which +exceptions can be thrown by it. ```nim -proc p1(): Future[void] {.asyncraises: [IOError].} = +proc p1(): Future[void] {.async, asyncraises: [IOError].} = assert not (compiles do: raise newException(ValueError, "uh-uh")) raise newException(IOError, "works") # Or any child of IOError ``` @@ -225,7 +225,7 @@ Under the hood, the return type of `p1` will be rewritten to another type, which will convey raises informations to await. ```nim -proc p2(): Future[void] {.asyncraises: [IOError].} = +proc p2(): Future[void] {.async, asyncraises: [IOError].} = await p1() # Works, because await knows that p1 # can only raise IOError ``` @@ -233,16 +233,16 @@ proc p2(): Future[void] {.asyncraises: [IOError].} = The hidden type (`RaiseTrackingFuture`) is implicitely convertible into a Future. However, it may causes issues when creating callback or methods ```nim -proc p3(): Future[void] {.asyncraises: [IOError].} = +proc p3(): Future[void] {.async, asyncraises: [IOError].} = let fut: Future[void] = p1() # works assert not compiles(await fut) # await lost informations about raises, # so it can raise anything # Callbacks assert not(compiles do: let cb1: proc(): Future[void] = p1) # doesn't work - let cb2: proc(): Future[void] {.asyncraises: [IOError].} = p1 # works + let cb2: proc(): Future[void] {.async, asyncraises: [IOError].} = p1 # works assert not(compiles do: - type c = proc(): Future[void] {.asyncraises: [IOError, ValueError].} + type c = proc(): Future[void] {.async, asyncraises: [IOError, ValueError].} let cb3: c = p1 # doesn't work, the raises must match _exactly_ ) ``` diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 9fb0d59e7..c8387025c 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -79,7 +79,7 @@ proc params2*(someProc: NimNode): NimNode = params(someProc) -proc asyncSingleProc(prc, raises: NimNode, trackExceptions: bool): 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, nnkProcTy}: @@ -87,22 +87,27 @@ proc asyncSingleProc(prc, raises: NimNode, trackExceptions: bool): NimNode {.com " proc/method definition or lambda node expected.") var - raisesTuple = - if raises.len > 0: - nnkTupleConstr.newTree() - else: - ident("void") + raisesTuple = nnkTupleConstr.newTree() foundRaises = -1 for index, pragma in pragma(prc): if pragma.kind == nnkExprColonExpr and pragma[0] == ident "raises": warning("The raises pragma doesn't work on async procedure. " & "Use asyncraises instead") + elif pragma.kind == nnkExprColonExpr and pragma[0] == ident "asyncraises": foundRaises = index - if foundRaises >= 0: pragma(prc).del(foundRaises) - for possibleRaise in raises: - raisesTuple.add(possibleRaise) + let trackExceptions = foundRaises >= 0 + if trackExceptions: + for possibleRaise in pragma(prc)[foundRaises][1]: + raisesTuple.add(possibleRaise) + if raisesTuple.len == 0: + raisesTuple = ident("void") + else: + const defaultException = + when defined(chronosStrictException): "CatchableError" + else: "Exception" + raisesTuple.add(ident(defaultException)) let returnType = prc.params2[0] var baseType: NimNode @@ -206,7 +211,10 @@ proc asyncSingleProc(prc, raises: NimNode, trackExceptions: bool): NimNode {.com # for more details. closureIterator.addPragma(newIdentNode("gcsafe")) - let closureRaises = raises.copy() + let closureRaises = nnkBracket.newNimNode() + for exception in raisesTuple: + closureRaises.add(exception) + closureRaises.add(ident("CancelledError")) when (NimMajor, NimMinor) < (1, 4): closureRaises.add(ident("Defect")) @@ -382,30 +390,17 @@ template awaitne*[T](f: Future[T]): Future[T] = else: unsupported "awaitne is only available within {.async.}" -proc asyncMultipleProcs( - prc, raises: NimNode, - trackExceptions: bool): NimNode {.compileTime.} = +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 = newStmtList() - result.add asyncSingleProc(oneProc, raises, trackExceptions) + result.add asyncSingleProc(oneProc) else: - result = asyncSingleProc(prc, raises, trackExceptions) + result = asyncSingleProc(prc) when defined(nimDumpAsync): echo repr result -macro async*(prc: untyped): untyped = - ## Macro which processes async procedures into the appropriate - ## iterators and yield statements. - - const defaultException = - when defined(chronosStrictException): "CatchableError" - else: "Exception" - let possibleExceptions = nnkBracket.newTree(newIdentNode(defaultException)) - asyncMultipleProcs(prc, possibleExceptions, false) - -macro asyncraises*(possibleExceptions, prc: untyped): untyped = - asyncMultipleProcs(prc, possibleExceptions, true) - -template asyncraises*(prc: untyped): untyped = - {.error: "Use .asyncraises: [].".} +template asyncraises*(possibleExceptions: untyped) {.pragma.} diff --git a/tests/testmacro.nim b/tests/testmacro.nim index ab5de0da3..f70466c02 100644 --- a/tests/testmacro.nim +++ b/tests/testmacro.nim @@ -86,22 +86,22 @@ suite "Exceptions tracking": check (not compiles(body)) test "Can raise valid exception": proc test1 {.async.} = raise newException(ValueError, "hey") - proc test2 {.asyncraises: [ValueError].} = raise newException(ValueError, "hey") - proc test3 {.asyncraises: [IOError, ValueError].} = + proc test2 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") + proc test3 {.async, asyncraises: [IOError, ValueError].} = if 1 == 2: raise newException(ValueError, "hey") else: raise newException(IOError, "hey") - proc test4 {.asyncraises: [].} = raise newException(Defect, "hey") - proc test5 {.asyncraises: [].} = await test5() + proc test4 {.async, asyncraises: [].} = raise newException(Defect, "hey") + proc test5 {.async, asyncraises: [].} = await test5() test "Cannot raise invalid exception": checkNotCompiles: - proc test3 {.asyncraises: [IOError].} = raise newException(ValueError, "hey") + proc test3 {.async, asyncraises: [IOError].} = raise newException(ValueError, "hey") test "Non-raising compatibility": - proc test1 {.asyncraises: [ValueError].} = raise newException(ValueError, "hey") + proc test1 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") let testVar: Future[void] = test1() proc test2 {.async.} = raise newException(ValueError, "hey") @@ -111,31 +111,31 @@ suite "Exceptions tracking": #let testVar3: proc: Future[void] = test1 test "Cannot store invalid future types": - proc test1 {.asyncraises: [ValueError].} = raise newException(ValueError, "hey") - proc test2 {.asyncraises: [IOError].} = raise newException(IOError, "hey") + proc test1 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") + proc test2 {.async, asyncraises: [IOError].} = raise newException(IOError, "hey") var a = test1() checkNotCompiles: a = test2() test "Await raises the correct types": - proc test1 {.asyncraises: [ValueError].} = raise newException(ValueError, "hey") - proc test2 {.asyncraises: [ValueError].} = await test1() + proc test1 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") + proc test2 {.async, asyncraises: [ValueError].} = await test1() checkNotCompiles: - proc test3 {.asyncraises: [].} = await test1() + proc test3 {.async, asyncraises: [].} = await test1() test "Can create callbacks": - proc test1 {.asyncraises: [ValueError].} = raise newException(ValueError, "hey") - let callback: proc {.asyncraises: [ValueError].} = test1 + proc test1 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") + let callback: proc {.async, asyncraises: [ValueError].} = test1 test "Can return values": - proc test1: Future[int] {.asyncraises: [ValueError].} = + proc test1: Future[int] {.async, asyncraises: [ValueError].} = if 1 == 0: raise newException(ValueError, "hey") return 12 - proc test2: Future[int] {.asyncraises: [ValueError, IOError].} = + proc test2: Future[int] {.async, asyncraises: [ValueError, IOError].} = return await test1() checkNotCompiles: - proc test3: Future[int] {.asyncraises: [].}= await test1() + proc test3: Future[int] {.async, asyncraises: [].} = await test1() check waitFor(test2()) == 12 From 0f46a06ac6373160fffe1e6276d953b9c6cacb7d Mon Sep 17 00:00:00 2001 From: Tanguy Date: Fri, 25 Feb 2022 12:44:14 +0100 Subject: [PATCH 19/31] Add -d:chronosWarnMissingRaises --- chronos/asyncmacro2.nim | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index c8387025c..1bae489bd 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -107,6 +107,8 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = const defaultException = when defined(chronosStrictException): "CatchableError" else: "Exception" + when defined(chronosWarnMissingRaises): + warning("Async proc miss asyncraises") raisesTuple.add(ident(defaultException)) let returnType = prc.params2[0] From 05e94c82f935558a49259174c128bbfe0d37bd44 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Fri, 25 Feb 2022 12:45:39 +0100 Subject: [PATCH 20/31] Add comment to RaiseTrackingFuture --- chronos/asyncfutures2.nim | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index 06d0478ff..d465d0d82 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -60,8 +60,9 @@ type closure*: iterator(f: Future[T]): FutureBase {.raises: [Defect, CatchableError, Exception], gcsafe.} value: T ## Stored value - # Future with a tuple of possible exception types - # eg RaiseTrackingFuture[void, (ValueError, OSError)] + ## Future with a tuple of possible exception types + ## eg RaiseTrackingFuture[void, (ValueError, OSError)] + ## Should generally not be used manually RaiseTrackingFuture*[T, E] = ref object of Future[T] FutureStr*[T] = ref object of Future[T] From 28ca5de5d31072fba2ce21c0411dcf32a3d0a020 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Fri, 4 Mar 2022 12:05:40 +0100 Subject: [PATCH 21/31] Allow standalone asyncraises --- chronos/asyncfutures2.nim | 11 ++- chronos/asyncmacro2.nim | 158 +++++++++++++++++++++++++++----------- tests/testmacro.nim | 11 +++ 3 files changed, 135 insertions(+), 45 deletions(-) diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index d465d0d82..d5a3fd58a 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -11,6 +11,7 @@ import std/[os, tables, strutils, heapqueue, options, deques, sequtils] import stew/base10 import ./srcloc +import macros export srcloc when defined(nimHasStacktracesModule): @@ -130,12 +131,18 @@ template newFuture*[T](fromProc: static[string] = ""): Future[T] = ## that this future belongs to, is a good habit as it helps with debugging. newFutureImpl[T](getSrcLocation(fromProc)) -template newRaiseTrackingFuture*[T, E](fromProc: static[string] = ""): RaiseTrackingFuture[T, E] = +macro getSubType(T: typedesc): untyped = + if getTypeInst(T)[1].len > 2: + getTypeInst(T)[1][2] + else: + ident"void" + +template newRaiseTrackingFuture*[T](fromProc: static[string] = ""): auto = ## Creates a new future. ## ## Specifying ``fromProc``, which is a string specifying the name of the proc ## that this future belongs to, is a good habit as it helps with debugging. - newRaiseTrackingFutureImpl[T, E](getSrcLocation(fromProc)) + newRaiseTrackingFutureImpl[T, getSubType(typeof(result))](getSrcLocation(fromProc)) template newFutureSeq*[A, B](fromProc: static[string] = ""): FutureSeq[A, B] = ## Create a new future which can hold/preserve GC sequence until future will diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 1bae489bd..8fabd60cd 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -63,7 +63,7 @@ proc getName(node: NimNode): string {.compileTime.} = error("Unknown name.") proc isInvalidReturnType(typeName: string): bool = - return typeName notin ["Future"] #, "FutureStream"] + return typeName notin ["Future", "RaiseTrackingFuture"] #, "FutureStream"] proc verifyReturnType(typeName: string) {.compileTime.} = if typeName.isInvalidReturnType: @@ -78,6 +78,11 @@ proc params2*(someProc: NimNode): NimNode = else: params(someProc) +template emptyRaises: NimNode = + when (NimMajor, NimMinor) < (1, 4): + nnkBracket.newTree(newIdentNode("Defect")) + else: + nnkBracket.newTree() proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = ## This macro transforms a single procedure into a closure iterator. @@ -86,23 +91,52 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = error("Cannot transform this node kind into an async proc." & " proc/method definition or lambda node expected.") + let returnType = prc.params2[0] + var baseType: NimNode + # Verify that the return type is a Future[T] + if returnType.kind == nnkBracketExpr: + let fut = repr(returnType[0]) + verifyReturnType(fut) + baseType = returnType[1] + elif returnType.kind in nnkCallKinds and returnType[0].eqIdent("[]"): + let fut = repr(returnType[1]) + verifyReturnType(fut) + baseType = returnType[2] + elif returnType.kind == nnkEmpty: + baseType = returnType + else: + error("Unhandled async return type: " & $prc.kind) + + let subtypeIsVoid = returnType.kind == nnkEmpty or + (baseType.kind == nnkIdent and returnType[1].eqIdent("void")) + var raisesTuple = nnkTupleConstr.newTree() foundRaises = -1 for index, pragma in pragma(prc): if pragma.kind == nnkExprColonExpr and pragma[0] == ident "raises": - warning("The raises pragma doesn't work on async procedure. " & - "Use asyncraises instead") + var wasAddedAutomatically = false + for index, pragma in pragma(prc): + if pragma.eqIdent("asyncinternal"): + wasAddedAutomatically = true + break + if not wasAddedAutomatically: + warning("The raises pragma doesn't work on async procedure. " & + "Use asyncraises instead") + elif pragma.kind == nnkExprColonExpr and pragma[0] == ident "asyncraises": foundRaises = index - let trackExceptions = foundRaises >= 0 - if trackExceptions: + if foundRaises >= 0: for possibleRaise in pragma(prc)[foundRaises][1]: raisesTuple.add(possibleRaise) if raisesTuple.len == 0: raisesTuple = ident("void") + elif returnType.kind == nnkBracketExpr and + returnType[0].eqIdent("RaiseTrackingFuture"): + # asyncraises was applied first + raisesTuple = returnType[2] else: const defaultException = when defined(chronosStrictException): "CatchableError" @@ -111,24 +145,6 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = warning("Async proc miss asyncraises") raisesTuple.add(ident(defaultException)) - let returnType = prc.params2[0] - var baseType: NimNode - # Verify that the return type is a Future[T] - if returnType.kind == nnkBracketExpr: - let fut = repr(returnType[0]) - verifyReturnType(fut) - baseType = returnType[1] - elif returnType.kind in nnkCallKinds and returnType[0].eqIdent("[]"): - let fut = repr(returnType[1]) - verifyReturnType(fut) - 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")) var outerProcBody = newNimNode(nnkStmtList, prc) @@ -139,20 +155,9 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = elif returnType.kind in nnkCallKinds and returnType[0].eqIdent("[]"): newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(returnType[2]) else: returnType - returnTypeWithException = - newNimNode(nnkBracketExpr). - add(newIdentNode("RaiseTrackingFuture")). - add(internalFutureType[1]). - add(raisesTuple) #Rewrite return type - if trackExceptions: - prc.params2[0] = nnkBracketExpr.newTree( - newIdentNode("RaiseTrackingFuture"), - internalFutureType[1], - raisesTuple - ) - elif subtypeIsVoid: + if subtypeIsVoid and returnType.kind == nnkEmpty: prc.params2[0] = internalFutureType # -> iterator nameIter(chronosInternalRetFuture: Future[T]): FutureBase {.closure.} = @@ -232,7 +237,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = closureIterator.addPragma(newIdentNode("gcsafe")) outerProcBody.add(closureIterator) - # -> var resultFuture = newRaiseTrackingFuture[T ,E]() + # -> var resultFuture = newRaiseTrackingFuture[T]() # declared at the end to be sure that the closure # doesn't reference it, avoid cyclic ref (#203) var retFutureSym = ident "resultFuture" @@ -246,7 +251,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = outerProcBody.add( newVarStmt( retFutureSym, - newCall(newTree(nnkBracketExpr, ident "newRaiseTrackingFuture", subRetType, raisesTuple), + newCall(newTree(nnkBracketExpr, ident "newRaiseTrackingFuture", subRetType), newLit(prcName)) ) ) @@ -270,15 +275,12 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = prc.addPragma(newColonExpr(ident "stackTrace", ident "off")) # The proc itself can't raise - let emptyRaises = - when (NimMajor, NimMinor) < (1, 4): - nnkBracket.newTree(newIdentNode("Defect")) - else: - nnkBracket.newTree() prc.addPragma(nnkExprColonExpr.newTree( newIdentNode("raises"), emptyRaises)) + prc.addPragma(newIdentNode("asyncinternal")) + # See **Remark 435** in this file. # https://github.com/nim-lang/RFCs/issues/435 prc.addPragma(newIdentNode("gcsafe")) @@ -290,6 +292,65 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = #if prcName == "recvLineInto": # echo(toStrLit(result)) +proc trackReturnType(prc, exceptions: NimNode): NimNode {.compileTime.} = + if prc.kind notin {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo, nnkProcTy}: + error("Cannot transform this node kind into an async proc." & + " proc/method definition or lambda node expected.") + + var raisesTuple = nnkTupleConstr.newTree() + + for possibleRaise in exceptions: + raisesTuple.add(possibleRaise) + if raisesTuple.len == 0: + raisesTuple = ident("void") + + for index, pragma in pragma(prc): + if pragma.kind == nnkExprColonExpr and pragma[0] == ident "raises": + var wasAddedAutomatically = false + for index, pragma in pragma(prc): + if pragma.eqIdent("asyncinternal"): + wasAddedAutomatically = true + break + if not wasAddedAutomatically: + warning("The raises pragma doesn't work on async procedure. " & + "Please remove it") + + let returnType = prc.params2[0] + var baseType: NimNode + # Verify that the return type is a Future[T] + if returnType.kind == nnkBracketExpr: + let fut = repr(returnType[0]) + verifyReturnType(fut) + baseType = returnType[1] + elif returnType.kind in nnkCallKinds and returnType[0].eqIdent("[]"): + let fut = repr(returnType[1]) + verifyReturnType(fut) + 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")) + + #Rewrite return type + prc.params2[0] = nnkBracketExpr.newTree( + newIdentNode("RaiseTrackingFuture"), + if subtypeIsVoid: ident"void" + else: baseType, + raisesTuple + ) + + # The proc itself can't raise + prc.addPragma(nnkExprColonExpr.newTree( + newIdentNode("raises"), + emptyRaises)) + + prc.addPragma(newIdentNode("asyncinternal")) + + prc + macro checkFutureExceptions(f, typ: typed): untyped = # For RaiseTrackingFuture[void, (ValueError, OSError), will do: # if isNil(f.error): discard @@ -405,4 +466,15 @@ macro async*(prc: untyped): untyped = when defined(nimDumpAsync): echo repr result -template asyncraises*(possibleExceptions: untyped) {.pragma.} +macro asyncraises*(possibleExceptions, prc: untyped): untyped = + if prc.kind == nnkStmtList: + result = newStmtList() + for oneProc in prc: + result.add trackReturnType(oneProc, possibleExceptions) + else: + result = trackReturnType(prc, possibleExceptions) + when defined(nimDumpAsync): + echo repr result + +template asyncinternal* {.pragma.} + # used to disarm some warnings diff --git a/tests/testmacro.nim b/tests/testmacro.nim index f70466c02..6dc1da59a 100644 --- a/tests/testmacro.nim +++ b/tests/testmacro.nim @@ -139,3 +139,14 @@ suite "Exceptions tracking": proc test3: Future[int] {.async, asyncraises: [].} = await test1() check waitFor(test2()) == 12 + + test "Standalone asyncraises": + proc test1: Future[int] {.asyncraises: [ValueError].} = + result = newRaiseTrackingFuture[int]() + result.complete(12) + check waitFor(test1()) == 12 + + test "Reversed async, asyncraises": + proc test44 {.asyncraises: [ValueError], async.} = raise newException(ValueError, "hey") + checkNotCompiles: + proc test33 {.asyncraises: [IOError], async.} = raise newException(ValueError, "hey") From f93409a60f53aaefa8cfc1d213cbc687ddc59d8e Mon Sep 17 00:00:00 2001 From: Tanguy Date: Mon, 14 Mar 2022 16:45:19 +0100 Subject: [PATCH 22/31] CheckedFuture.fail type checking --- chronos/asyncfutures2.nim | 43 +++++++++++++++++++++++++++++++++++++++ tests/testmacro.nim | 17 +++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index d5a3fd58a..f82ae4b55 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -273,6 +273,49 @@ template fail*[T](future: Future[T], error: ref CatchableError) = ## Completes ``future`` with ``error``. fail(future, error, getSrcLocation()) +macro checkFailureType(future, error: typed): untyped = + let e = getTypeInst(future)[2] + let types = getType(e) + + if types.eqIdent("void"): + error("Can't raise exceptions on this Future") + + expectKind(types, nnkBracketExpr) + expectKind(types[0], nnkSym) + assert types[0].strVal == "tuple" + assert types.len > 1 + + expectKind(getTypeInst(error), nnkRefTy) + let toMatch = getTypeInst(error)[0] + + # Can't find a way to check `is` in the macro. (sameType doesn't + # work for inherited objects). Dirty hack here, for [IOError, OSError], + # this will generate: + # + # static: + # if not((`toMatch` is IOError) or (`toMatch` is OSError) + # or (`toMatch` is CancelledError) or false): + # raiseAssert("Can't fail with `toMatch`, only [IOError, OSError] is allowed") + var typeChecker = ident"false" + + for errorType in types[1..^1]: + typeChecker = newCall("or", typeChecker, newCall("is", toMatch, errorType)) + typeChecker = newCall( + "or", typeChecker, + newCall("is", toMatch, ident"CancelledError")) + + let errorMsg = "Can't fail with " & repr(toMatch) & ". Only " & repr(types[1..^1]) & " allowed" + + result = nnkStaticStmt.newNimNode(lineInfoFrom=error).add( + quote do: + if not(`typeChecker`): + raiseAssert(`errorMsg`) + ) + +template fail*[T, E](future: RaiseTrackingFuture[T, E], error: ref CatchableError) = + checkFailureType(future, error) + fail(future, error, getSrcLocation()) + template newCancelledError(): ref CancelledError = (ref CancelledError)(msg: "Future operation cancelled!") diff --git a/tests/testmacro.nim b/tests/testmacro.nim index 6dc1da59a..7bf77e48a 100644 --- a/tests/testmacro.nim +++ b/tests/testmacro.nim @@ -140,12 +140,27 @@ suite "Exceptions tracking": check waitFor(test2()) == 12 - test "Standalone asyncraises": + test "Manual tracking": proc test1: Future[int] {.asyncraises: [ValueError].} = result = newRaiseTrackingFuture[int]() result.complete(12) check waitFor(test1()) == 12 + proc test2: Future[int] {.asyncraises: [IOError, OSError].} = + result = newRaiseTrackingFuture[int]() + result.fail(newException(IOError, "fail")) + result.fail(newException(OSError, "fail")) + checkNotCompiles: + result.fail(newException(ValueError, "fail")) + + proc test3: Future[void] {.asyncraises: [].} = + checkNotCompiles: + result.fail(newException(ValueError, "fail")) + + # Inheritance + proc test4: Future[void] {.asyncraises: [CatchableError].} = + result.fail(newException(IOError, "fail")) + test "Reversed async, asyncraises": proc test44 {.asyncraises: [ValueError], async.} = raise newException(ValueError, "hey") checkNotCompiles: From c3f8882d2930a281081134a99e7260b98574fd94 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Wed, 30 Mar 2022 16:26:24 +0200 Subject: [PATCH 23/31] First cleanup --- chronos/asyncfutures2.nim | 4 +- chronos/asyncmacro2.nim | 174 ++++++++++++++++---------------------- 2 files changed, 73 insertions(+), 105 deletions(-) diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index f82ae4b55..cf8eea4fa 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -131,7 +131,7 @@ template newFuture*[T](fromProc: static[string] = ""): Future[T] = ## that this future belongs to, is a good habit as it helps with debugging. newFutureImpl[T](getSrcLocation(fromProc)) -macro getSubType(T: typedesc): untyped = +macro getFutureExceptions(T: typedesc): untyped = if getTypeInst(T)[1].len > 2: getTypeInst(T)[1][2] else: @@ -142,7 +142,7 @@ template newRaiseTrackingFuture*[T](fromProc: static[string] = ""): auto = ## ## Specifying ``fromProc``, which is a string specifying the name of the proc ## that this future belongs to, is a good habit as it helps with debugging. - newRaiseTrackingFutureImpl[T, getSubType(typeof(result))](getSrcLocation(fromProc)) + newRaiseTrackingFutureImpl[T, getFutureExceptions(typeof(result))](getSrcLocation(fromProc)) template newFutureSeq*[A, B](fromProc: static[string] = ""): FutureSeq[A, B] = ## Create a new future which can hold/preserve GC sequence until future will diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 34e91102e..3de325876 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -72,7 +72,7 @@ proc verifyReturnType(typeName: string) {.compileTime.} = macro unsupported(s: static[string]): untyped = error s -proc params2*(someProc: NimNode): NimNode = +proc params2(someProc: NimNode): NimNode = # until https://github.com/nim-lang/Nim/pull/19563 is available if someProc.kind == nnkProcTy: someProc[0] @@ -99,46 +99,55 @@ template emptyRaises: NimNode = else: nnkBracket.newTree() -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. +template asyncinternal* {.pragma.} + # used to disarm the raises warning + # when the raises was added automatically + +proc getBaseType(prc: NimNode): NimNode {.compileTime.} = if prc.kind notin {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo, nnkProcTy}: error("Cannot transform this node kind into an async proc." & " proc/method definition or lambda node expected.") - let returnType = cleanupOpenSymChoice(prc.params[0]) - let prcName = prc.name.getName + # Warn about raises usage + let pragmas = pragma(prc) + for index, pragma in pragmas: + if pragma.kind == nnkExprColonExpr and pragma[0] == ident "raises": + if pragmas.hasCustomPragma(asyncinternal): + warning("The raises pragma doesn't work on async procedure. " & + "Please remove it") + + let returnType = cleanupOpenSymChoice(prc.params2[0]) - var baseType: NimNode # Verify that the return type is a Future[T] + # and extract the BaseType from it if returnType.kind == nnkBracketExpr: let fut = repr(returnType[0]) verifyReturnType(fut) - baseType = returnType[1] + returnType[1] elif returnType.kind == nnkEmpty: - baseType = returnType + ident("void") else: error("Unhandled async return type: " & $prc.kind) + # error isn't noreturn.. + return - let subtypeIsVoid = returnType.kind == nnkEmpty or - (baseType.kind == nnkIdent and returnType[1].eqIdent("void")) + +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. + + let + baseType = getBaseType(prc) + prcName = prc.name.getName + returnType = cleanupOpenSymChoice(prc.params2[0]) + subtypeIsVoid = baseType.eqIdent("void") var raisesTuple = nnkTupleConstr.newTree() foundRaises = -1 for index, pragma in pragma(prc): - if pragma.kind == nnkExprColonExpr and pragma[0] == ident "raises": - var wasAddedAutomatically = false - for index, pragma in pragma(prc): - if pragma.eqIdent("asyncinternal"): - wasAddedAutomatically = true - break - if not wasAddedAutomatically: - warning("The raises pragma doesn't work on async procedure. " & - "Use asyncraises instead") - - elif pragma.kind == nnkExprColonExpr and pragma[0] == ident "asyncraises": + if pragma.kind == nnkExprColonExpr and pragma[0] == ident "asyncraises": foundRaises = index if foundRaises >= 0: @@ -158,21 +167,15 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = warning("Async proc miss asyncraises") raisesTuple.add(ident(defaultException)) + # Rewrite the implicit Future[void] + if subtypeIsVoid and returnType.kind == nnkEmpty: + prc.params2[0] = + newNimNode(nnkBracketExpr, prc). + add(newIdentNode("Future")). + add(newIdentNode("void")) var outerProcBody = newNimNode(nnkStmtList, prc) - let - internalFutureType = - if subtypeIsVoid: - newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(newIdentNode("void")) - elif returnType.kind in nnkCallKinds and returnType[0].eqIdent("[]"): - newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(returnType[2]) - else: returnType - - #Rewrite return type - if subtypeIsVoid and returnType.kind == nnkEmpty: - prc.params2[0] = internalFutureType - # -> iterator nameIter(chronosInternalRetFuture: Future[T]): FutureBase {.closure.} = # -> {.push warning[resultshadowed]: off.} # -> var result: T @@ -216,7 +219,11 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = # -> complete(chronosInternalRetFuture) procBody.add(newCall(newIdentNode("complete"), internalFutureSym)) - let internalFutureParameter = nnkIdentDefs.newTree(internalFutureSym, internalFutureType, newEmptyNode()) + let internalFutureParameter = + nnkIdentDefs.newTree( + internalFutureSym, + newNimNode(nnkBracketExpr, prc).add(newIdentNode("Future")).add(baseType), + newEmptyNode()) var closureIterator = newProc(iteratorNameSym, [newIdentNode("FutureBase"), internalFutureParameter], procBody, nnkIteratorDef) closureIterator.pragma = newNimNode(nnkPragma, lineInfoFrom=prc.body) @@ -254,17 +261,12 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = # 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 "newRaiseTrackingFuture", subRetType), + newCall(newTree(nnkBracketExpr, ident "newRaiseTrackingFuture", baseType), newLit(prcName)) ) ) @@ -305,65 +307,6 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = #if prcName == "recvLineInto": # echo(toStrLit(result)) -proc trackReturnType(prc, exceptions: NimNode): NimNode {.compileTime.} = - if prc.kind notin {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo, nnkProcTy}: - error("Cannot transform this node kind into an async proc." & - " proc/method definition or lambda node expected.") - - var raisesTuple = nnkTupleConstr.newTree() - - for possibleRaise in exceptions: - raisesTuple.add(possibleRaise) - if raisesTuple.len == 0: - raisesTuple = ident("void") - - for index, pragma in pragma(prc): - if pragma.kind == nnkExprColonExpr and pragma[0] == ident "raises": - var wasAddedAutomatically = false - for index, pragma in pragma(prc): - if pragma.eqIdent("asyncinternal"): - wasAddedAutomatically = true - break - if not wasAddedAutomatically: - warning("The raises pragma doesn't work on async procedure. " & - "Please remove it") - - let returnType = prc.params2[0] - var baseType: NimNode - # Verify that the return type is a Future[T] - if returnType.kind == nnkBracketExpr: - let fut = repr(returnType[0]) - verifyReturnType(fut) - baseType = returnType[1] - elif returnType.kind in nnkCallKinds and returnType[0].eqIdent("[]"): - let fut = repr(returnType[1]) - verifyReturnType(fut) - 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")) - - #Rewrite return type - prc.params2[0] = nnkBracketExpr.newTree( - newIdentNode("RaiseTrackingFuture"), - if subtypeIsVoid: ident"void" - else: baseType, - raisesTuple - ) - - # The proc itself can't raise - prc.addPragma(nnkExprColonExpr.newTree( - newIdentNode("raises"), - emptyRaises)) - - prc.addPragma(newIdentNode("asyncinternal")) - - prc - macro checkFutureExceptions(f, typ: typed): untyped = # For RaiseTrackingFuture[void, (ValueError, OSError), will do: # if isNil(f.error): discard @@ -479,6 +422,34 @@ macro async*(prc: untyped): untyped = when defined(nimDumpAsync): echo repr result +proc trackReturnType(prc, exceptions: NimNode): NimNode {.compileTime.} = + var raisesTuple = nnkTupleConstr.newTree() + + for possibleRaise in exceptions: + raisesTuple.add(possibleRaise) + if raisesTuple.len == 0: + raisesTuple = ident("void") + + let baseType = getBaseType(prc) + + #Rewrite return type + prc.params2[0] = nnkBracketExpr.newTree( + newIdentNode("RaiseTrackingFuture"), + baseType, + raisesTuple + ) + + # The proc itself can't raise + prc.addPragma(nnkExprColonExpr.newTree( + newIdentNode("raises"), + emptyRaises)) + + # This is used to inform `.async.` that the raises pragma + # was added by us. Otherwise, it would pop a warning + prc.addPragma(newIdentNode("asyncinternal")) + + prc + macro asyncraises*(possibleExceptions, prc: untyped): untyped = if prc.kind == nnkStmtList: result = newStmtList() @@ -488,6 +459,3 @@ macro asyncraises*(possibleExceptions, prc: untyped): untyped = result = trackReturnType(prc, possibleExceptions) when defined(nimDumpAsync): echo repr result - -template asyncinternal* {.pragma.} - # used to disarm some warnings From d91151aafeae4c11e9749d2c73164e0d7dbaa26e Mon Sep 17 00:00:00 2001 From: Tanguy Date: Wed, 30 Mar 2022 16:31:48 +0200 Subject: [PATCH 24/31] Remove useless line --- chronos/asyncmacro2.nim | 1 - 1 file changed, 1 deletion(-) diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 3de325876..19b34c312 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -138,7 +138,6 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = let baseType = getBaseType(prc) - prcName = prc.name.getName returnType = cleanupOpenSymChoice(prc.params2[0]) subtypeIsVoid = baseType.eqIdent("void") From b9a20befa072b2bfb68db71b3400da11cb7b61f7 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Fri, 3 Jun 2022 11:23:47 +0200 Subject: [PATCH 25/31] Review comments --- chronos/asyncfutures2.nim | 7 ++++--- chronos/asyncmacro2.nim | 12 ++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index cf8eea4fa..a7ceea59a 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -61,10 +61,11 @@ type closure*: iterator(f: Future[T]): FutureBase {.raises: [Defect, CatchableError, Exception], gcsafe.} value: T ## Stored value - ## Future with a tuple of possible exception types - ## eg RaiseTrackingFuture[void, (ValueError, OSError)] - ## Should generally not be used manually RaiseTrackingFuture*[T, E] = ref object of Future[T] + ## Future with a tuple of possible exception types + ## eg RaiseTrackingFuture[void, (ValueError, OSError)] + ## Will be injected by `asyncraises`, should generally + ## not be used manually FutureStr*[T] = ref object of Future[T] ## Future to hold GC strings diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 19b34c312..40c8fc627 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -309,9 +309,9 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = macro checkFutureExceptions(f, typ: typed): untyped = # For RaiseTrackingFuture[void, (ValueError, OSError), will do: # if isNil(f.error): discard - # elif f.error of type CancelledError: raise cast[ref CancelledError](f.error) - # elif f.error of type ValueError: raise cast[ref ValueError](f.error) - # elif f.error of type OSError: raise cast[ref OSError](f.error) + # elif f.error of type CancelledError: raise (ref CancelledError)(f.error) + # elif f.error of type ValueError: raise (ref ValueError)(f.error) + # elif f.error of type OSError: raise (ref OSError)(f.error) # else: raiseAssert("Unhandled future exception: " & f.error.msg) # # In future nim versions, this could simply be @@ -324,7 +324,7 @@ macro checkFutureExceptions(f, typ: typed): untyped = return quote do: if not(isNil(`f`.error)): if `f`.error of type CancelledError: - raise cast[ref CancelledError](`f`.error) + raise (ref CancelledError)(`f`.error) else: raiseAssert("Unhandled future exception: " & `f`.error.msg) @@ -343,14 +343,14 @@ macro checkFutureExceptions(f, typ: typed): untyped = result.add nnkElifExpr.newTree( quote do: `f`.error of type CancelledError, nnkRaiseStmt.newNimNode(lineInfoFrom=typ).add( - quote do: cast[ref CancelledError](`f`.error) + quote do: (ref CancelledError)(`f`.error) ) ) for errorType in types[1..^1]: result.add nnkElifExpr.newTree( quote do: `f`.error of type `errorType`, nnkRaiseStmt.newNimNode(lineInfoFrom=typ).add( - quote do: cast[ref `errorType`](`f`.error) + quote do: (ref `errorType`)(`f`.error) ) ) From 20716d28c66e3d747e134787eb1f798d3124f49a Mon Sep 17 00:00:00 2001 From: Tanguy Date: Fri, 3 Jun 2022 11:25:29 +0200 Subject: [PATCH 26/31] nimble: Remove #head from unittest2 --- chronos.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chronos.nimble b/chronos.nimble index 4a71ae20d..e8dcbe034 100644 --- a/chronos.nimble +++ b/chronos.nimble @@ -11,7 +11,7 @@ requires "nim > 1.2.0", "stew", "bearssl", "httputils", - "https://github.com/status-im/nim-unittest2.git#head" + "https://github.com/status-im/nim-unittest2.git" var commandStart = "nim c -r --hints:off --verbosity:0 --skipParentCfg:on --warning[ObservableStores]:off --styleCheck:usages --styleCheck:error" From d5eed32a7ae3245a0a41eac32b215f8bb58cb205 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Tue, 7 Jun 2022 10:59:50 +0200 Subject: [PATCH 27/31] Remove implict raises: CancelledError --- chronos/asyncmacro2.nim | 13 +------------ tests/testmacro.nim | 10 +++++----- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 684783f18..f412937a1 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -243,7 +243,6 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = for exception in raisesTuple: closureRaises.add(exception) - closureRaises.add(ident("CancelledError")) when (NimMajor, NimMinor) < (1, 4): closureRaises.add(ident("Defect")) @@ -311,7 +310,6 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = macro checkFutureExceptions(f, typ: typed): untyped = # For RaiseTrackingFuture[void, (ValueError, OSError), will do: # if isNil(f.error): discard - # elif f.error of type CancelledError: raise (ref CancelledError)(f.error) # elif f.error of type ValueError: raise (ref ValueError)(f.error) # elif f.error of type OSError: raise (ref OSError)(f.error) # else: raiseAssert("Unhandled future exception: " & f.error.msg) @@ -325,10 +323,7 @@ macro checkFutureExceptions(f, typ: typed): untyped = if types.eqIdent("void"): return quote do: if not(isNil(`f`.error)): - if `f`.error of type CancelledError: - raise (ref CancelledError)(`f`.error) - else: - raiseAssert("Unhandled future exception: " & `f`.error.msg) + raiseAssert("Unhandled future exception: " & `f`.error.msg) expectKind(types, nnkBracketExpr) expectKind(types[0], nnkSym) @@ -342,12 +337,6 @@ macro checkFutureExceptions(f, typ: typed): untyped = ) ) - result.add nnkElifExpr.newTree( - quote do: `f`.error of type CancelledError, - nnkRaiseStmt.newNimNode(lineInfoFrom=typ).add( - quote do: (ref CancelledError)(`f`.error) - ) - ) for errorType in types[1..^1]: result.add nnkElifExpr.newTree( quote do: `f`.error of type `errorType`, diff --git a/tests/testmacro.nim b/tests/testmacro.nim index 87a4efcc8..a37b11157 100644 --- a/tests/testmacro.nim +++ b/tests/testmacro.nim @@ -95,7 +95,7 @@ suite "Exceptions tracking": raise newException(IOError, "hey") proc test4 {.async, asyncraises: [].} = raise newException(Defect, "hey") - proc test5 {.async, asyncraises: [].} = await test5() + proc test5 {.async, asyncraises: [CancelledError].} = await test5() test "Cannot raise invalid exception": checkNotCompiles: @@ -121,9 +121,9 @@ suite "Exceptions tracking": test "Await raises the correct types": proc test1 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") - proc test2 {.async, asyncraises: [ValueError].} = await test1() + proc test2 {.async, asyncraises: [ValueError, CancelledError].} = await test1() checkNotCompiles: - proc test3 {.async, asyncraises: [].} = await test1() + proc test3 {.async, asyncraises: [CancelledError].} = await test1() test "Can create callbacks": proc test1 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") @@ -133,11 +133,11 @@ suite "Exceptions tracking": proc test1: Future[int] {.async, asyncraises: [ValueError].} = if 1 == 0: raise newException(ValueError, "hey") return 12 - proc test2: Future[int] {.async, asyncraises: [ValueError, IOError].} = + proc test2: Future[int] {.async, asyncraises: [ValueError, IOError, CancelledError].} = return await test1() checkNotCompiles: - proc test3: Future[int] {.async, asyncraises: [].} = await test1() + proc test3: Future[int] {.async, asyncraises: [CancelledError].} = await test1() check waitFor(test2()) == 12 From 046722e16f6fc7ffde14598eb342aed09cb99154 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Fri, 22 Jul 2022 16:54:58 +0200 Subject: [PATCH 28/31] Move checkFutureExceptions to asyncfutures2 --- chronos/asyncfutures2.nim | 42 +++++++++++++++++++++++++++++++++++++++ chronos/asyncmacro2.nim | 42 --------------------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index a7ceea59a..cd3279cf4 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -571,6 +571,48 @@ proc internalRead*[T](fut: Future[T]): T {.inline.} = when T isnot void: return fut.value +macro checkFutureExceptions*(f, typ: typed): untyped = + # For RaiseTrackingFuture[void, (ValueError, OSError), will do: + # if isNil(f.error): discard + # elif f.error of type ValueError: raise (ref ValueError)(f.error) + # elif f.error of type OSError: raise (ref OSError)(f.error) + # else: raiseAssert("Unhandled future exception: " & f.error.msg) + # + # In future nim versions, this could simply be + # {.cast(raises: [ValueError, OSError]).}: + # raise f.error + let e = getTypeInst(typ)[2] + let types = getType(e) + + if types.eqIdent("void"): + return quote do: + if not(isNil(`f`.error)): + raiseAssert("Unhandled future exception: " & `f`.error.msg) + + expectKind(types, nnkBracketExpr) + expectKind(types[0], nnkSym) + assert types[0].strVal == "tuple" + assert types.len > 1 + + result = nnkIfExpr.newTree( + nnkElifExpr.newTree( + quote do: isNil(`f`.error), + quote do: discard + ) + ) + + for errorType in types[1..^1]: + result.add nnkElifExpr.newTree( + quote do: `f`.error of type `errorType`, + nnkRaiseStmt.newNimNode(lineInfoFrom=typ).add( + quote do: (ref `errorType`)(`f`.error) + ) + ) + + result.add nnkElseExpr.newTree( + quote do: raiseAssert("Unhandled future exception: " & `f`.error.msg) + ) + proc read*[T](future: Future[T] ): T {. raises: [Defect, CatchableError].} = ## Retrieves the value of ``future``. Future must be finished otherwise diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index f412937a1..08e44ab48 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -307,48 +307,6 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = #if prcName == "recvLineInto": # echo(toStrLit(result)) -macro checkFutureExceptions(f, typ: typed): untyped = - # For RaiseTrackingFuture[void, (ValueError, OSError), will do: - # if isNil(f.error): discard - # elif f.error of type ValueError: raise (ref ValueError)(f.error) - # elif f.error of type OSError: raise (ref OSError)(f.error) - # else: raiseAssert("Unhandled future exception: " & f.error.msg) - # - # In future nim versions, this could simply be - # {.cast(raises: [ValueError, OSError]).}: - # raise f.error - let e = getTypeInst(typ)[2] - let types = getType(e) - - if types.eqIdent("void"): - return quote do: - if not(isNil(`f`.error)): - raiseAssert("Unhandled future exception: " & `f`.error.msg) - - expectKind(types, nnkBracketExpr) - expectKind(types[0], nnkSym) - assert types[0].strVal == "tuple" - assert types.len > 1 - - result = nnkIfExpr.newTree( - nnkElifExpr.newTree( - quote do: isNil(`f`.error), - quote do: discard - ) - ) - - for errorType in types[1..^1]: - result.add nnkElifExpr.newTree( - quote do: `f`.error of type `errorType`, - nnkRaiseStmt.newNimNode(lineInfoFrom=typ).add( - quote do: (ref `errorType`)(`f`.error) - ) - ) - - result.add nnkElseExpr.newTree( - quote do: raiseAssert("Unhandled future exception: " & `f`.error.msg) - ) - template await*[T](f: Future[T]): untyped = when declared(chronosInternalRetFuture): #work around https://github.com/nim-lang/Nim/issues/19193 From c3aa399f621a511420063debf256d301ae6e33d7 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Wed, 7 Sep 2022 11:55:47 +0200 Subject: [PATCH 29/31] Small refacto --- chronos/asyncfutures2.nim | 42 --------- chronos/asyncmacro2.nim | 184 +++++++++++++++++++++----------------- 2 files changed, 100 insertions(+), 126 deletions(-) diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index cd3279cf4..a7ceea59a 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -571,48 +571,6 @@ proc internalRead*[T](fut: Future[T]): T {.inline.} = when T isnot void: return fut.value -macro checkFutureExceptions*(f, typ: typed): untyped = - # For RaiseTrackingFuture[void, (ValueError, OSError), will do: - # if isNil(f.error): discard - # elif f.error of type ValueError: raise (ref ValueError)(f.error) - # elif f.error of type OSError: raise (ref OSError)(f.error) - # else: raiseAssert("Unhandled future exception: " & f.error.msg) - # - # In future nim versions, this could simply be - # {.cast(raises: [ValueError, OSError]).}: - # raise f.error - let e = getTypeInst(typ)[2] - let types = getType(e) - - if types.eqIdent("void"): - return quote do: - if not(isNil(`f`.error)): - raiseAssert("Unhandled future exception: " & `f`.error.msg) - - expectKind(types, nnkBracketExpr) - expectKind(types[0], nnkSym) - assert types[0].strVal == "tuple" - assert types.len > 1 - - result = nnkIfExpr.newTree( - nnkElifExpr.newTree( - quote do: isNil(`f`.error), - quote do: discard - ) - ) - - for errorType in types[1..^1]: - result.add nnkElifExpr.newTree( - quote do: `f`.error of type `errorType`, - nnkRaiseStmt.newNimNode(lineInfoFrom=typ).add( - quote do: (ref `errorType`)(`f`.error) - ) - ) - - result.add nnkElseExpr.newTree( - quote do: raiseAssert("Unhandled future exception: " & `f`.error.msg) - ) - proc read*[T](future: Future[T] ): T {. raises: [Defect, CatchableError].} = ## Retrieves the value of ``future``. Future must be finished otherwise diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 08e44ab48..9b90b8563 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -7,17 +7,10 @@ # distribution, for details about the copyright. # -import std/[macros] - -proc skipUntilStmtList(node: NimNode): NimNode {.compileTime.} = - # Skips a nest of StmtList's. - result = node - if node[0].kind == nnkStmtList: - result = skipUntilStmtList(node[0]) +import std/[macros, algorithm] proc processBody(node, retFutureSym: NimNode, subTypeIsVoid: bool): NimNode {.compileTime.} = - #echo(node.treeRepr) result = node case node.kind of nnkReturnStmt: @@ -62,11 +55,8 @@ proc getName(node: NimNode): string {.compileTime.} = else: error("Unknown name.") -proc isInvalidReturnType(typeName: string): bool = - return typeName notin ["Future", "RaiseTrackingFuture"] #, "FutureStream"] - proc verifyReturnType(typeName: string) {.compileTime.} = - if typeName.isInvalidReturnType: + if typeName notin ["Future", "RaiseTrackingFuture"]: #, "FutureStream"] error("Expected return type of 'Future' got '" & typeName & "'") macro unsupported(s: static[string]): untyped = @@ -98,23 +88,11 @@ template emptyRaises: NimNode = else: nnkBracket.newTree() -template asyncinternal* {.pragma.} - # used to disarm the raises warning - # when the raises was added automatically - proc getBaseType(prc: NimNode): NimNode {.compileTime.} = if prc.kind notin {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo, nnkProcTy}: error("Cannot transform this node kind into an async proc." & " proc/method definition or lambda node expected.") - # Warn about raises usage - let pragmas = pragma(prc) - for index, pragma in pragmas: - if pragma.kind == nnkExprColonExpr and pragma[0] == ident "raises": - if pragmas.hasCustomPragma(asyncinternal): - warning("The raises pragma doesn't work on async procedure. " & - "Please remove it") - let returnType = cleanupOpenSymChoice(prc.params2[0]) # Verify that the return type is a Future[T] @@ -130,48 +108,78 @@ proc getBaseType(prc: NimNode): NimNode {.compileTime.} = # error isn't noreturn.. return - 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. let baseType = getBaseType(prc) - returnType = cleanupOpenSymChoice(prc.params2[0]) subtypeIsVoid = baseType.eqIdent("void") var raisesTuple = nnkTupleConstr.newTree() foundRaises = -1 + foundAsync = -1 for index, pragma in pragma(prc): if pragma.kind == nnkExprColonExpr and pragma[0] == ident "asyncraises": foundRaises = index + elif pragma.eqIdent("async"): + foundAsync = index + elif pragma.kind == nnkExprColonExpr and pragma[0] == ident "raises": + warning("The raises pragma doesn't work on async procedure. " & + "Please remove it or use asyncraises instead") if foundRaises >= 0: for possibleRaise in pragma(prc)[foundRaises][1]: raisesTuple.add(possibleRaise) if raisesTuple.len == 0: raisesTuple = ident("void") - elif returnType.kind == nnkBracketExpr and - returnType[0].eqIdent("RaiseTrackingFuture"): - # asyncraises was applied first - raisesTuple = returnType[2] else: + when defined(chronosWarnMissingRaises): + warning("Async proc miss asyncraises") + const defaultException = when defined(chronosStrictException): "CatchableError" else: "Exception" - when defined(chronosWarnMissingRaises): - warning("Async proc miss asyncraises") raisesTuple.add(ident(defaultException)) - # Rewrite the implicit Future[void] - if subtypeIsVoid and returnType.kind == nnkEmpty: + + if foundRaises >= 0: + # Rewrite to RaiseTrackingFuture + prc.params2[0] = nnkBracketExpr.newTree( + newIdentNode("RaiseTrackingFuture"), + baseType, + raisesTuple + ) + elif subtypeIsVoid: + # Rewrite the implicit Future[void] prc.params2[0] = newNimNode(nnkBracketExpr, prc). add(newIdentNode("Future")). add(newIdentNode("void")) + # Remove pragmas + let toRemoveList = @[foundRaises, foundAsync].filterIt(it >= 0).sorted().reversed() + for toRemove in toRemoveList: + pragma(prc).del(toRemove) + + if prc.kind != nnkLambda and prc.kind != nnkProcTy: # TODO: Nim bug? + prc.addPragma(newColonExpr(ident "stackTrace", ident "off")) + + # The proc itself can't raise + prc.addPragma(nnkExprColonExpr.newTree( + newIdentNode("raises"), + emptyRaises)) + + # See **Remark 435** in this file. + # https://github.com/nim-lang/RFCs/issues/435 + prc.addPragma(newIdentNode("gcsafe")) + + if foundAsync < 0: + return prc + + # Transform to closure iter var outerProcBody = newNimNode(nnkStmtList, prc) # Copy comment for nimdoc if prc.body.len > 0 and prc.body[0].kind == nnkCommentStmt: @@ -286,26 +294,55 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = # -> return resultFuture outerProcBody.add newNimNode(nnkReturnStmt, prc.body[^1]).add(retFutureSym) - if prc.kind != nnkLambda and prc.kind != nnkProcTy: # TODO: Nim bug? - prc.addPragma(newColonExpr(ident "stackTrace", ident "off")) - - # The proc itself can't raise - prc.addPragma(nnkExprColonExpr.newTree( - newIdentNode("raises"), - emptyRaises)) - - prc.addPragma(newIdentNode("asyncinternal")) - - # See **Remark 435** in this file. - # https://github.com/nim-lang/RFCs/issues/435 - prc.addPragma(newIdentNode("gcsafe")) result = prc if procBody.kind != nnkEmpty: result.body = outerProcBody - #echo(treeRepr(result)) - #if prcName == "recvLineInto": - # echo(toStrLit(result)) + + when defined(nimDumpAsync): + echo repr result + +macro checkFutureExceptions*(f, typ: typed): untyped = + # For RaiseTrackingFuture[void, (ValueError, OSError), will do: + # if isNil(f.error): discard + # elif f.error of type ValueError: raise (ref ValueError)(f.error) + # elif f.error of type OSError: raise (ref OSError)(f.error) + # else: raiseAssert("Unhandled future exception: " & f.error.msg) + # + # In future nim versions, this could simply be + # {.cast(raises: [ValueError, OSError]).}: + # raise f.error + let e = getTypeInst(typ)[2] + let types = getType(e) + + if types.eqIdent("void"): + return quote do: + if not(isNil(`f`.error)): + raiseAssert("Unhandled future exception: " & `f`.error.msg) + + expectKind(types, nnkBracketExpr) + expectKind(types[0], nnkSym) + assert types[0].strVal == "tuple" + assert types.len > 1 + + result = nnkIfExpr.newTree( + nnkElifExpr.newTree( + quote do: isNil(`f`.error), + quote do: discard + ) + ) + + for errorType in types[1..^1]: + result.add nnkElifExpr.newTree( + quote do: `f`.error of type `errorType`, + nnkRaiseStmt.newNimNode(lineInfoFrom=typ).add( + quote do: (ref `errorType`)(`f`.error) + ) + ) + + result.add nnkElseExpr.newTree( + quote do: raiseAssert("Unhandled future exception: " & `f`.error.msg) + ) template await*[T](f: Future[T]): untyped = when declared(chronosInternalRetFuture): @@ -364,46 +401,25 @@ macro async*(prc: untyped): untyped = if prc.kind == nnkStmtList: result = newStmtList() for oneProc in prc: + oneProc.addPragma(ident"async") result.add asyncSingleProc(oneProc) else: + prc.addPragma(ident"async") result = asyncSingleProc(prc) - when defined(nimDumpAsync): - echo repr result - -proc trackReturnType(prc, exceptions: NimNode): NimNode {.compileTime.} = - var raisesTuple = nnkTupleConstr.newTree() - - for possibleRaise in exceptions: - raisesTuple.add(possibleRaise) - if raisesTuple.len == 0: - raisesTuple = ident("void") - - let baseType = getBaseType(prc) - - #Rewrite return type - prc.params2[0] = nnkBracketExpr.newTree( - newIdentNode("RaiseTrackingFuture"), - baseType, - raisesTuple - ) - - # The proc itself can't raise - prc.addPragma(nnkExprColonExpr.newTree( - newIdentNode("raises"), - emptyRaises)) - - # This is used to inform `.async.` that the raises pragma - # was added by us. Otherwise, it would pop a warning - prc.addPragma(newIdentNode("asyncinternal")) - - prc macro asyncraises*(possibleExceptions, prc: untyped): untyped = + # Add back the pragma and let asyncSingleProc handle it if prc.kind == nnkStmtList: result = newStmtList() for oneProc in prc: - result.add trackReturnType(oneProc, possibleExceptions) + oneProc.addPragma(nnkExprColonExpr.newTree( + ident"asyncraises", + possibleExceptions + )) + result.add asyncSingleProc(oneProc) else: - result = trackReturnType(prc, possibleExceptions) - when defined(nimDumpAsync): - echo repr result + prc.addPragma(nnkExprColonExpr.newTree( + ident"asyncraises", + possibleExceptions + )) + result = asyncSingleProc(prc) From ea08a8fbab677e59b81ea84b89e410aacebb5bb5 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Wed, 7 Sep 2022 19:20:18 +0200 Subject: [PATCH 30/31] Async raises of, first pass --- chronos/asyncfutures2.nim | 20 +++++++- chronos/asyncmacro2.nim | 100 +++++++++++++++++++++++++++++++++++--- tests/testmacro.nim | 35 +++++++++++++ 3 files changed, 147 insertions(+), 8 deletions(-) diff --git a/chronos/asyncfutures2.nim b/chronos/asyncfutures2.nim index a7ceea59a..b9352a66f 100644 --- a/chronos/asyncfutures2.nim +++ b/chronos/asyncfutures2.nim @@ -286,8 +286,14 @@ macro checkFailureType(future, error: typed): untyped = assert types[0].strVal == "tuple" assert types.len > 1 - expectKind(getTypeInst(error), nnkRefTy) - let toMatch = getTypeInst(error)[0] + let toMatch = + if getTypeInst(error).kind == nnkRefTy: + getTypeInst(error)[0] + elif getTypeInst(error).kind == nnkBracketExpr: + getTypeInst(error)[1] + else: + error("Unhandled error type " & $getTypeInst(error).kind) + nil # Can't find a way to check `is` in the macro. (sameType doesn't # work for inherited objects). Dirty hack here, for [IOError, OSError], @@ -317,6 +323,16 @@ template fail*[T, E](future: RaiseTrackingFuture[T, E], error: ref CatchableErro checkFailureType(future, error) fail(future, error, getSrcLocation()) +macro checkMultipleFailureType(future, errors: typed): untyped = + result = nnkStmtList.newTree() + for error in getTypeInst(errors): + let err2 = ident(error.strVal) + result.add(quote do: checkFailureType(`future`, `err2`)) + +template fail*[T1, E1, T2, E2](future: RaiseTrackingFuture[T1, E1], failedFuture: RaiseTrackingFuture[T2, E2]) = + checkMultipleFailureType(future, E2) + fail(Future[T1](future), failedFuture.error, getSrcLocation()) + template newCancelledError(): ref CancelledError = (ref CancelledError)(msg: "Future operation cancelled!") diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 9b90b8563..7c83f7326 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -108,6 +108,23 @@ proc getBaseType(prc: NimNode): NimNode {.compileTime.} = # error isn't noreturn.. return +macro combineAsyncExceptions*(args: varargs[typed]): untyped = + result = nnkTupleConstr.newTree() + for argRaw in args: + let arg = getTypeInst(argRaw) + if arg.kind == nnkBracketExpr and arg[0].eqIdent("typeDesc"): + if arg[1].kind == nnkSym: + result.add(ident(arg[1].strVal)) + elif arg[1].kind == nnkTupleConstr: + for subArg in arg[1]: + result.add(ident(subArg.strVal)) + else: + error("Unhandled exception subarg source" & $arg[1].kind) + else: + error("Unhandled exception source" & $arg.kind) + + echo treeRepr(result) + 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. @@ -119,11 +136,14 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = var raisesTuple = nnkTupleConstr.newTree() foundRaises = -1 + foundRaisesOf = -1 foundAsync = -1 for index, pragma in pragma(prc): if pragma.kind == nnkExprColonExpr and pragma[0] == ident "asyncraises": foundRaises = index + elif pragma.kind == nnkExprColonExpr and pragma[0] == ident "asyncraisesof": + foundRaisesOf = index elif pragma.eqIdent("async"): foundAsync = index elif pragma.kind == nnkExprColonExpr and pragma[0] == ident "raises": @@ -145,7 +165,49 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = raisesTuple.add(ident(defaultException)) - if foundRaises >= 0: + var genericErrorTypes: seq[NimNode] + if foundRaisesOf >= 0: + + if prc[2].kind == nnkEmpty: + prc[2] = nnkGenericParams.newTree() + prc.params2[0] = ident"auto" + + for index, raisesOf in pragma(prc)[foundRaisesOf][1]: + let prcParams = params2(prc) + for index, param in prcParams: + if index == 0: continue # return type + + if param[0].eqIdent(raisesOf): + if param[1].kind != nnkBracketExpr or (param[1][0].eqIdent("Future") == false): + error "asyncraisesof only applies to Future parameters" + param[1][0] = ident"RaiseTrackingFuture" + + let genericSym = genSym(kind=nskGenericParam, ident="Err" & $index) + genericErrorTypes.add(genericSym) + prc[2].add nnkIdentDefs.newTree( + genericSym, + newEmptyNode(), + newEmptyNode() + ) + param[1].add(genericSym) + + if prc.body.kind != nnkEmpty and foundAsync < 0: + let macroCall = newCall("combineAsyncExceptions") + for a in raisesTuple: macroCall.add(a) + for a in genericErrorTypes: macroCall.add(ident(a.strVal)) + prc.body.insert(0, + newAssignment( + ident"result", + newCall( + nnkBracketExpr.newTree( + ident"RaiseTrackingFuture", + baseType, + macroCall), + newNilLit() + ) + ) + ) + elif foundRaises >= 0: # Rewrite to RaiseTrackingFuture prc.params2[0] = nnkBracketExpr.newTree( newIdentNode("RaiseTrackingFuture"), @@ -159,8 +221,10 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = add(newIdentNode("Future")). add(newIdentNode("void")) + echo repr(prc) + # Remove pragmas - let toRemoveList = @[foundRaises, foundAsync].filterIt(it >= 0).sorted().reversed() + let toRemoveList = @[foundRaises, foundAsync, foundRaisesOf].filterIt(it >= 0).sorted().reversed() for toRemove in toRemoveList: pragma(prc).del(toRemove) @@ -254,10 +318,17 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = when (NimMajor, NimMinor) < (1, 4): closureRaises.add(ident("Defect")) - closureIterator.addPragma(nnkExprColonExpr.newTree( - newIdentNode("raises"), - closureRaises - )) + if foundRaisesOf < 0: + closureIterator.addPragma(nnkExprColonExpr.newTree( + newIdentNode("raises"), + closureRaises + )) + else: + for a in genericErrorTypes: closureRaises.add(ident(a.strVal)) + closureIterator.addPragma(nnkExprColonExpr.newTree( + newIdentNode("asyncinternalraises"), + closureRaises + )) # If proc has an explicit gcsafe pragma, we add it to iterator as well. if prc.pragma.findChild(it.kind in {nnkSym, nnkIdent} and @@ -423,3 +494,20 @@ macro asyncraises*(possibleExceptions, prc: untyped): untyped = possibleExceptions )) result = asyncSingleProc(prc) + +macro asyncraisesof*(raisesof, prc: untyped): untyped = + # Add back the pragma and let asyncSingleProc handle it + if prc.kind == nnkStmtList: + result = newStmtList() + for oneProc in prc: + oneProc.addPragma(nnkExprColonExpr.newTree( + ident"asyncraisesof", + raisesof + )) + result.add asyncSingleProc(oneProc) + else: + prc.addPragma(nnkExprColonExpr.newTree( + ident"asyncraisesof", + raisesof + )) + result = asyncSingleProc(prc) diff --git a/tests/testmacro.nim b/tests/testmacro.nim index ba24afb43..a79764cdb 100644 --- a/tests/testmacro.nim +++ b/tests/testmacro.nim @@ -189,6 +189,41 @@ suite "Exceptions tracking": macroAsync2(testMacro2, seq, Opt, Result, OpenObject, cstring) check waitFor(testMacro2()).len == 0 + test "asyncRaisesOf - manual async": + proc test44 {.asyncraises: [ValueError], async.} = discard + + proc testOr[T, Y](fut1: Future[T], fut2: Future[Y]): Future[void] {.asyncraises: [CancelledError], asyncraisesof: [fut1, fut2].} = + var retFuture = newRaiseTrackingFuture[void]("chronos.or") + var cb: proc(udata: pointer) {.gcsafe, raises: [Defect].} + cb = proc(udata: pointer) {.gcsafe, raises: [Defect].} = + if not(retFuture.finished()): + var fut = cast[FutureBase](udata) + if cast[pointer](fut1) == udata: + fut2.removeCallback(cb) + if fut.failed(): + retFuture.fail(fut1) + return + else: + fut1.removeCallback(cb) + if fut.failed(): + retFuture.fail(fut2) + return + retFuture.complete() + + fut1.addCallback(cb) + fut2.addCallback(cb) + + return retFuture + + waitFor(testOr(test44(), test44())) + + test "asyncRaisesOf - macro async": + proc test44 {.asyncraises: [ValueError], async.} = discard + + proc wrapper1(fut: Future[void]) {.asyncraises: [CancelledError], async, asyncraisesof: [fut].} = + await fut + waitFor(wrapper1(test44())) + suite "async transformation issues": test "Nested defer/finally not called on return": # issue #288 From 10629537d8304d6188f32229ea33b5fe3917f148 Mon Sep 17 00:00:00 2001 From: Tanguy Date: Tue, 8 Nov 2022 18:02:49 +0100 Subject: [PATCH 31/31] Working asyncraisesof --- chronos/asyncmacro2.nim | 47 ++++++++++++++++++++++++++--------------- tests/testmacro.nim | 15 ++++++++++++- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/chronos/asyncmacro2.nim b/chronos/asyncmacro2.nim index 7c83f7326..5e04d3dc4 100644 --- a/chronos/asyncmacro2.nim +++ b/chronos/asyncmacro2.nim @@ -108,8 +108,7 @@ proc getBaseType(prc: NimNode): NimNode {.compileTime.} = # error isn't noreturn.. return -macro combineAsyncExceptions*(args: varargs[typed]): untyped = - result = nnkTupleConstr.newTree() +proc combineAsyncExceptionsHelper(args: NimNode): seq[NimNode] = for argRaw in args: let arg = getTypeInst(argRaw) if arg.kind == nnkBracketExpr and arg[0].eqIdent("typeDesc"): @@ -123,7 +122,22 @@ macro combineAsyncExceptions*(args: varargs[typed]): untyped = else: error("Unhandled exception source" & $arg.kind) - echo treeRepr(result) +macro combineAsyncExceptions*(args: varargs[typed]): untyped = + result = nnkTupleConstr.newTree() + for argRaw in combineAsyncExceptionsHelper(args): + result.add(argRaw) + +macro asyncinternalraises*(prc, exceptions: typed): untyped = + let closureRaises = nnkBracket.newNimNode() + for argRaw in combineAsyncExceptionsHelper(exceptions): + closureRaises.add(argRaw) + + prc.addPragma(nnkExprColonExpr.newTree( + newIdentNode("raises"), + closureRaises + )) + + prc proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = ## This macro transforms a single procedure into a closure iterator. @@ -166,6 +180,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = var genericErrorTypes: seq[NimNode] + var resultTypeSetter: NimNode if foundRaisesOf >= 0: if prc[2].kind == nnkEmpty: @@ -191,11 +206,10 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = ) param[1].add(genericSym) - if prc.body.kind != nnkEmpty and foundAsync < 0: let macroCall = newCall("combineAsyncExceptions") for a in raisesTuple: macroCall.add(a) for a in genericErrorTypes: macroCall.add(ident(a.strVal)) - prc.body.insert(0, + resultTypeSetter = newAssignment( ident"result", newCall( @@ -206,7 +220,8 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = newNilLit() ) ) - ) + if prc.body.kind != nnkEmpty and foundAsync < 0: + prc.body.insert(0, resultTypeSetter) elif foundRaises >= 0: # Rewrite to RaiseTrackingFuture prc.params2[0] = nnkBracketExpr.newTree( @@ -221,8 +236,6 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = add(newIdentNode("Future")). add(newIdentNode("void")) - echo repr(prc) - # Remove pragmas let toRemoveList = @[foundRaises, foundAsync, foundRaisesOf].filterIt(it >= 0).sorted().reversed() for toRemove in toRemoveList: @@ -318,6 +331,11 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = when (NimMajor, NimMinor) < (1, 4): closureRaises.add(ident("Defect")) + # 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")) + if foundRaisesOf < 0: closureIterator.addPragma(nnkExprColonExpr.newTree( newIdentNode("raises"), @@ -325,15 +343,8 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = )) else: for a in genericErrorTypes: closureRaises.add(ident(a.strVal)) - closureIterator.addPragma(nnkExprColonExpr.newTree( - newIdentNode("asyncinternalraises"), - closureRaises - )) + closureIterator = newCall("asyncinternalraises", closureIterator, closureRaises) - # 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 = newRaiseTrackingFuture[T]() @@ -342,6 +353,8 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = var retFutureSym = ident "resultFuture" # Do not change this code to `quote do` version because `instantiationInfo` # will be broken for `newFuture()` call. + if not isNil(resultTypeSetter): + outerProcBody.add(resultTypeSetter) outerProcBody.add( newVarStmt( retFutureSym, @@ -349,7 +362,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = newLit(prcName)) ) ) - + # -> resultFuture.closure = iterator outerProcBody.add( newAssignment( diff --git a/tests/testmacro.nim b/tests/testmacro.nim index a79764cdb..9cf23a03c 100644 --- a/tests/testmacro.nim +++ b/tests/testmacro.nim @@ -194,6 +194,10 @@ suite "Exceptions tracking": proc testOr[T, Y](fut1: Future[T], fut2: Future[Y]): Future[void] {.asyncraises: [CancelledError], asyncraisesof: [fut1, fut2].} = var retFuture = newRaiseTrackingFuture[void]("chronos.or") + checkNotCompiles: + # Should only allow ValueError here + retFuture.fail(newException(IOError, "eh")) + var cb: proc(udata: pointer) {.gcsafe, raises: [Defect].} cb = proc(udata: pointer) {.gcsafe, raises: [Defect].} = if not(retFuture.finished()): @@ -219,10 +223,19 @@ suite "Exceptions tracking": test "asyncRaisesOf - macro async": proc test44 {.asyncraises: [ValueError], async.} = discard + proc testOther {.asyncraises: [IOError], async.} = discard proc wrapper1(fut: Future[void]) {.asyncraises: [CancelledError], async, asyncraisesof: [fut].} = await fut - waitFor(wrapper1(test44())) + + checkNotCompiles: + proc wrapper2(fut: Future[void]) {.asyncraises: [CancelledError], async, asyncraisesof: [fut].} = + await testOther() + waitFor wrapper2(test44()) + + proc test55 {.asyncraises: [ValueError, CancelledError], async.} = + await wrapper1(test44()) + waitFor(test55()) suite "async transformation issues": test "Nested defer/finally not called on return":