From cd6369c0488e1bc1dd6b6ce2fbc3b372a8adb74f Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Tue, 7 Nov 2023 11:12:59 +0100 Subject: [PATCH] `asyncraises` -> `async: (raises: ..., raw: ...)` (#455) Per discussion in https://github.com/status-im/nim-chronos/pull/251#issuecomment-1559233139, `async: (parameters..)` is introduced as a way to customize the async transformation instead of relying on separate keywords (like asyncraises). Two parameters are available as of now: `raises`: controls the exception effect tracking `raw`: disables body transformation Parameters are added to `async` as a tuple allowing more params to be added easily in the future: ```nim: proc f() {.async: (name: value, ...).}` ``` --- README.md | 110 +++++++++------- chronos/asyncloop.nim | 2 +- chronos/internal/asyncengine.nim | 2 +- chronos/internal/asyncfutures.nim | 43 +++---- chronos/internal/asyncmacro.nim | 195 +++++++++++++++-------------- chronos/internal/raisesfutures.nim | 28 +++-- tests/testmacro.nim | 64 +++++----- 7 files changed, 239 insertions(+), 205 deletions(-) diff --git a/README.md b/README.md index 495f9f8..f59a6c8 100644 --- a/README.md +++ b/README.md @@ -46,18 +46,18 @@ Submit a PR to add yours! ### Concepts -Chronos implements the async/await paradigm in a self-contained library, using -macros, with no specific helpers from the compiler. +Chronos implements the async/await paradigm in a self-contained library using +the macro and closure iterator transformation features provided by Nim. -Our event loop is called a "dispatcher" and a single instance per thread is +The event loop is called a "dispatcher" and a single instance per thread is created, as soon as one is needed. To trigger a dispatcher's processing step, we need to call `poll()` - either -directly or through a wrapper like `runForever()` or `waitFor()`. This step +directly or through a wrapper like `runForever()` or `waitFor()`. Each step handles any file descriptors, timers and callbacks that are ready to be processed. -`Future` objects encapsulate the result of an async procedure, upon successful +`Future` objects encapsulate the result of an `async` procedure upon successful completion, and a list of callbacks to be scheduled after any type of completion - be that success, failure or cancellation. @@ -156,7 +156,7 @@ Exceptions inheriting from `CatchableError` are caught by hidden `try` blocks and placed in the `Future.error` field, changing the future's status to `Failed`. -When a future is awaited, that exception is re-raised, only to be caught again +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 exceptions move up the async chain. @@ -214,57 +214,81 @@ by the transformation. #### Checked exceptions -By specifying a `asyncraises` list to an async procedure, you can check which -exceptions can be thrown by it. +By specifying a `raises` list to an async procedure, you can check which +exceptions can be raised by it: + ```nim -proc p1(): Future[void] {.async, asyncraises: [IOError].} = +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 -``` -Under the hood, the return type of `p1` will be rewritten to an internal type, -which will convey raises informations to `await`. - -```nim -proc p2(): Future[void] {.async, asyncraises: [IOError].} = +proc p2(): Future[void] {.async, (raises: [IOError]).} = await p1() # Works, because await knows that p1 # can only raise IOError ``` -Raw functions and callbacks that don't go through the `async` transformation but -still return a `Future` and interact with the rest of the framework also need to -be annotated with `asyncraises` to participate in the checked exception scheme: +Under the hood, the return type of `p1` will be rewritten to an internal type +which will convey raises informations to `await`. + +### Raw functions + +Raw functions are those that interact with `chronos` via the `Future` type but +whose body does not go through the async transformation. + +Such functions are created by adding `raw: true` to the `async` parameters: ```nim -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] {.async, asyncraises: [IOError].} = p1 # works - assert not(compiles do: - type c = proc(): Future[void] {.async, asyncraises: [IOError, ValueError].} - let cb3: c = p1 # doesn't work, the raises must match _exactly_ - ) +proc rawAsync(): Future[void] {.async: (raw: true).} = + let future = newFuture[void]("rawAsync") + future.complete() + return future ``` -When `chronos` performs the `async` transformation, all code is placed in a -a special `try/except` clause that re-routes exception handling to the `Future`. - -Beacuse of this re-routing, functions that return a `Future` instance manually -never directly raise exceptions themselves - instead, exceptions are handled -indirectly via `await` or `Future.read`. When writing raw async functions, they -too must not raise exceptions - instead, they must store exceptions in the -future they return: +Raw functions must not raise exceptions directly - they are implicitly declared +as `raises: []` - instead they should store exceptions in the returned `Future`: ```nim -proc p4(): Future[void] {.asyncraises: [ValueError].} = - let fut = newFuture[void] +proc rawFailure(): Future[void] {.async: (raw: true).} = + let future = newFuture[void]("rawAsync") + future.fail((ref ValueError)(msg: "Oh no!")) + return future +``` - # Equivalent of `raise (ref ValueError)()` in raw async functions: - fut.fail((ref ValueError)(msg: "raising in raw async function")) - fut +Raw functions can also use checked exceptions: + +```nim +proc rawAsyncRaises(): Future[void] {.async: (raw: true, raises: [IOError]).} = + let fut = newFuture[void]() + assert not (compiles do: fut.fail((ref ValueError)(msg: "uh-uh"))) + fut.fail((ref IOError)(msg: "IO")) + return fut +``` + +### Callbacks and closures + +Callback/closure types are declared using the `async` annotation as usual: + +```nim +type MyCallback = proc(): Future[void] {.async.} + +proc runCallback(cb: MyCallback) {.async: (raises: []).} = + try: + await cb() + except CatchableError: + discard # handle errors as usual +``` + +When calling a callback, it is important to remember that the given function +may raise and exceptions need to be handled. + +Checked exceptions can be used to limit the exceptions that a callback can +raise: + +```nim +type MyEasyCallback = proc: Future[void] {.async: (raises: []).} + +proc runCallback(cb: MyEasyCallback) {.async: (raises: [])} = + await cb() ``` ### Platform independence @@ -278,7 +302,7 @@ annotated as raising `CatchableError` only raise on _some_ platforms - in order to work on all platforms, calling code must assume that they will raise even when they don't seem to do so on one platform. -### Exception effects +### Strict exception mode `chronos` currently offers minimal support for exception effects and `raises` annotations. In general, during the `async` transformation, a generic diff --git a/chronos/asyncloop.nim b/chronos/asyncloop.nim index b4d48af..428252c 100644 --- a/chronos/asyncloop.nim +++ b/chronos/asyncloop.nim @@ -131,4 +131,4 @@ import ./internal/[asyncengine, asyncfutures, asyncmacro, errors] export asyncfutures, asyncengine, errors -export asyncmacro.async, asyncmacro.await, asyncmacro.awaitne, asyncraises +export asyncmacro.async, asyncmacro.await, asyncmacro.awaitne diff --git a/chronos/internal/asyncengine.nim b/chronos/internal/asyncengine.nim index ebcc278..23d7c6a 100644 --- a/chronos/internal/asyncengine.nim +++ b/chronos/internal/asyncengine.nim @@ -21,7 +21,7 @@ export Port export deques, errors, futures, timer, results export - asyncmacro.async, asyncmacro.await, asyncmacro.awaitne, asyncmacro.asyncraises + asyncmacro.async, asyncmacro.await, asyncmacro.awaitne const MaxEventsCount* = 64 diff --git a/chronos/internal/asyncfutures.nim b/chronos/internal/asyncfutures.nim index abf28c7..b144cea 100644 --- a/chronos/internal/asyncfutures.nim +++ b/chronos/internal/asyncfutures.nim @@ -102,18 +102,12 @@ template newFuture*[T](fromProc: static[string] = "", else: newFutureImpl[T](getSrcLocation(fromProc), flags) -macro getFutureExceptions(T: typedesc): untyped = - if getTypeInst(T)[1].len > 2: - getTypeInst(T)[1][2] - else: - ident"void" - -template newInternalRaisesFuture*[T](fromProc: static[string] = ""): auto = +template newInternalRaisesFuture*[T, E](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. - newInternalRaisesFutureImpl[T, getFutureExceptions(typeof(result))](getSrcLocation(fromProc)) + newInternalRaisesFutureImpl[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 @@ -476,7 +470,7 @@ macro internalCheckComplete*(f: InternalRaisesFuture): untyped = let e = getTypeInst(f)[2] let types = getType(e) - if types.eqIdent("void"): + if isNoRaises(types): return quote do: if not(isNil(`f`.internalError)): raiseAssert("Unhandled future exception: " & `f`.error.msg) @@ -484,7 +478,6 @@ macro internalCheckComplete*(f: InternalRaisesFuture): untyped = expectKind(types, nnkBracketExpr) expectKind(types[0], nnkSym) assert types[0].strVal == "tuple" - assert types.len > 1 let ifRaise = nnkIfExpr.newTree( nnkElifExpr.newTree( @@ -914,7 +907,7 @@ template cancel*(future: FutureBase) {. cancelSoon(future, nil, nil, getSrcLocation()) proc cancelAndWait*(future: FutureBase, loc: ptr SrcLoc): Future[void] {. - asyncraises: [CancelledError].} = + async: (raw: true, raises: [CancelledError]).} = ## Perform cancellation ``future`` return Future which will be completed when ## ``future`` become finished (completed with value, failed or cancelled). ## @@ -938,7 +931,7 @@ template cancelAndWait*(future: FutureBase): Future[void] = ## Cancel ``future``. cancelAndWait(future, getSrcLocation()) -proc noCancel*[F: SomeFuture](future: F): auto = # asyncraises: asyncraiseOf(future) - CancelledError +proc noCancel*[F: SomeFuture](future: F): auto = # async: (raw: true, raises: asyncraiseOf(future) - CancelledError ## Prevent cancellation requests from propagating to ``future`` while ## forwarding its value or error when it finishes. ## @@ -978,7 +971,7 @@ proc noCancel*[F: SomeFuture](future: F): auto = # asyncraises: asyncraiseOf(fut retFuture proc allFutures*(futs: varargs[FutureBase]): Future[void] {. - asyncraises: [CancelledError].} = + async: (raw: true, raises: [CancelledError]).} = ## Returns a future which will complete only when all futures in ``futs`` ## will be completed, failed or canceled. ## @@ -1017,7 +1010,7 @@ proc allFutures*(futs: varargs[FutureBase]): Future[void] {. retFuture proc allFutures*[T](futs: varargs[Future[T]]): Future[void] {. - asyncraises: [CancelledError].} = + async: (raw: true, raises: [CancelledError]).} = ## Returns a future which will complete only when all futures in ``futs`` ## will be completed, failed or canceled. ## @@ -1031,7 +1024,7 @@ proc allFutures*[T](futs: varargs[Future[T]]): Future[void] {. allFutures(nfuts) proc allFinished*[F: SomeFuture](futs: varargs[F]): Future[seq[F]] {. - asyncraises: [CancelledError].} = + async: (raw: true, raises: [CancelledError]).} = ## Returns a future which will complete only when all futures in ``futs`` ## will be completed, failed or canceled. ## @@ -1072,7 +1065,7 @@ proc allFinished*[F: SomeFuture](futs: varargs[F]): Future[seq[F]] {. return retFuture proc one*[F: SomeFuture](futs: varargs[F]): Future[F] {. - asyncraises: [ValueError, CancelledError].} = + async: (raw: true, raises: [ValueError, CancelledError]).} = ## Returns a future which will complete and return completed Future[T] inside, ## when one of the futures in ``futs`` will be completed, failed or canceled. ## @@ -1121,7 +1114,7 @@ proc one*[F: SomeFuture](futs: varargs[F]): Future[F] {. return retFuture proc race*(futs: varargs[FutureBase]): Future[FutureBase] {. - asyncraises: [CancelledError].} = + async: (raw: true, raises: [CancelledError]).} = ## Returns a future which will complete and return completed FutureBase, ## when one of the futures in ``futs`` will be completed, failed or canceled. ## @@ -1173,7 +1166,8 @@ proc race*(futs: varargs[FutureBase]): Future[FutureBase] {. when (chronosEventEngine in ["epoll", "kqueue"]) or defined(windows): import std/os - proc waitSignal*(signal: int): Future[void] {.asyncraises: [AsyncError, CancelledError].} = + proc waitSignal*(signal: int): Future[void] {. + async: (raw: true, raises: [AsyncError, CancelledError]).} = var retFuture = newFuture[void]("chronos.waitSignal()") var signalHandle: Opt[SignalHandle] @@ -1208,7 +1202,7 @@ when (chronosEventEngine in ["epoll", "kqueue"]) or defined(windows): retFuture proc sleepAsync*(duration: Duration): Future[void] {. - asyncraises: [CancelledError].} = + async: (raw: true, raises: [CancelledError]).} = ## Suspends the execution of the current async procedure for the next ## ``duration`` time. var retFuture = newFuture[void]("chronos.sleepAsync(Duration)") @@ -1228,10 +1222,12 @@ proc sleepAsync*(duration: Duration): Future[void] {. return retFuture proc sleepAsync*(ms: int): Future[void] {. - inline, deprecated: "Use sleepAsync(Duration)", asyncraises: [CancelledError].} = + inline, deprecated: "Use sleepAsync(Duration)", + async: (raw: true, raises: [CancelledError]).} = result = sleepAsync(ms.milliseconds()) -proc stepsAsync*(number: int): Future[void] {.asyncraises: [CancelledError].} = +proc stepsAsync*(number: int): Future[void] {. + async: (raw: true, raises: [CancelledError]).} = ## Suspends the execution of the current async procedure for the next ## ``number`` of asynchronous steps (``poll()`` calls). ## @@ -1258,7 +1254,8 @@ proc stepsAsync*(number: int): Future[void] {.asyncraises: [CancelledError].} = retFuture -proc idleAsync*(): Future[void] {.asyncraises: [CancelledError].} = +proc idleAsync*(): Future[void] {. + async: (raw: true, raises: [CancelledError]).} = ## Suspends the execution of the current asynchronous task until "idle" time. ## ## "idle" time its moment of time, when no network events were processed by @@ -1277,7 +1274,7 @@ proc idleAsync*(): Future[void] {.asyncraises: [CancelledError].} = retFuture proc withTimeout*[T](fut: Future[T], timeout: Duration): Future[bool] {. - asyncraises: [CancelledError].} = + async: (raw: true, raises: [CancelledError]).} = ## Returns a future which will complete once ``fut`` completes or after ## ``timeout`` milliseconds has elapsed. ## diff --git a/chronos/internal/asyncmacro.nim b/chronos/internal/asyncmacro.nim index f313f6f..c110847 100644 --- a/chronos/internal/asyncmacro.nim +++ b/chronos/internal/asyncmacro.nim @@ -9,8 +9,9 @@ # import - std/[algorithm, macros, sequtils], - ../[futures, config] + std/[macros], + ../[futures, config], + ./raisesfutures proc processBody(node, setResultSym, baseType: NimNode): NimNode {.compileTime.} = case node.kind @@ -32,10 +33,10 @@ proc processBody(node, setResultSym, baseType: NimNode): NimNode {.compileTime.} node[i] = processBody(node[i], setResultSym, baseType) node -proc wrapInTryFinally(fut, baseType, body, raisesTuple: NimNode): NimNode {.compileTime.} = +proc wrapInTryFinally(fut, baseType, body, raises: NimNode): NimNode {.compileTime.} = # creates: # try: `body` - # [for raise in raisesTuple]: + # [for raise in raises]: # except `raise`: closureSucceeded = false; `castFutureSym`.fail(exc) # finally: # if closureSucceeded: @@ -91,7 +92,17 @@ proc wrapInTryFinally(fut, baseType, body, raisesTuple: NimNode): NimNode {.comp newCall(ident "fail", fut, excName) )) - for exc in raisesTuple: + let raises = if raises == nil: + const defaultException = + when defined(chronosStrictException): "CatchableError" + else: "Exception" + nnkTupleConstr.newTree(ident(defaultException)) + elif isNoRaises(raises): + nnkTupleConstr.newTree() + else: + raises + + for exc in raises: if exc.eqIdent("Exception"): addCancelledError addCatchableError @@ -182,42 +193,33 @@ proc cleanupOpenSymChoice(node: NimNode): NimNode {.compileTime.} = for child in node: result.add(cleanupOpenSymChoice(child)) -proc getAsyncCfg(prc: NimNode): tuple[raises: bool, async: bool, raisesTuple: NimNode] = - # reads the pragmas to extract the useful data - # and removes them +proc decodeParams(params: NimNode): tuple[raw: bool, raises: NimNode] = + # decodes the parameter tuple given in `async: (name: value, ...)` to its + # recognised parts + params.expectKind(nnkTupleConstr) + var - foundRaises = -1 - foundAsync = -1 + raw = false + raises: NimNode = nil - 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") + for param in params: + param.expectKind(nnkExprColonExpr) - result.raises = foundRaises >= 0 - result.async = foundAsync >= 0 - result.raisesTuple = nnkTupleConstr.newTree() + if param[0].eqIdent("raises"): + param[1].expectKind(nnkBracket) + if param[1].len == 0: + raises = makeNoRaises() + else: + raises = nnkTupleConstr.newTree() + for possibleRaise in param[1]: + raises.add(possibleRaise) + elif param[0].eqIdent("raw"): + # boolVal doesn't work in untyped macros it seems.. + raw = param[1].eqIdent("true") + else: + warning("Unrecognised async parameter: " & repr(param[0]), param) - if foundRaises >= 0: - for possibleRaise in pragma(prc)[foundRaises][1]: - result.raisesTuple.add(possibleRaise) - if result.raisesTuple.len == 0: - result.raisesTuple = ident("void") - else: - when defined(chronosWarnMissingRaises): - warning("Async proc miss asyncraises") - const defaultException = - when defined(chronosStrictException): "CatchableError" - else: "Exception" - result.raisesTuple.add(ident(defaultException)) - - let toRemoveList = @[foundRaises, foundAsync].filterIt(it >= 0).sorted().reversed() - for toRemove in toRemoveList: - pragma(prc).del(toRemove) + (raw, raises) proc isEmpty(n: NimNode): bool {.compileTime.} = # true iff node recursively contains only comments or empties @@ -230,13 +232,18 @@ proc isEmpty(n: NimNode): bool {.compileTime.} = else: false -proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = +proc asyncSingleProc(prc, params: 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 {nnkProcTy, nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}: error("Cannot transform " & $prc.kind & " into an async proc." & " proc/method definition or lambda node expected.", prc) + for pragma in prc.pragma(): + if pragma.kind == nnkExprColonExpr and pragma[0].eqIdent("raises"): + warning("The raises pragma doesn't work on async procedures - use " & + "`async: (raises: [...]) instead.", prc) + let returnType = cleanupOpenSymChoice(prc.params2[0]) # Verify that the return type is a Future[T] @@ -254,22 +261,24 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = let baseTypeIsVoid = baseType.eqIdent("void") - futureVoidType = nnkBracketExpr.newTree(ident "Future", ident "void") - (hasRaises, isAsync, raisesTuple) = getAsyncCfg(prc) - - if hasRaises: - # Store `asyncraises` types in InternalRaisesFuture - prc.params2[0] = nnkBracketExpr.newTree( - newIdentNode("InternalRaisesFuture"), - baseType, - raisesTuple - ) - elif baseTypeIsVoid: - # Adds the implicit Future[void] - prc.params2[0] = + (raw, raises) = decodeParams(params) + internalFutureType = + if baseTypeIsVoid: newNimNode(nnkBracketExpr, prc). add(newIdentNode("Future")). - add(newIdentNode("void")) + add(baseType) + else: + returnType + internalReturnType = if raises == nil: + internalFutureType + else: + nnkBracketExpr.newTree( + newIdentNode("InternalRaisesFuture"), + baseType, + raises + ) + + prc.params2[0] = internalReturnType if prc.kind notin {nnkProcTy, nnkLambda}: # TODO: Nim bug? prc.addPragma(newColonExpr(ident "stackTrace", ident "off")) @@ -282,24 +291,28 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = # https://github.com/nim-lang/RFCs/issues/435 prc.addPragma(newIdentNode("gcsafe")) - if isAsync == false: # `asyncraises` without `async` - # type InternalRaisesFutureRaises {.used.} = `raisesTuple` - # `body` - prc.body = nnkStmtList.newTree( - nnkTypeSection.newTree( - nnkTypeDef.newTree( - nnkPragmaExpr.newTree( - ident"InternalRaisesFutureRaises", - nnkPragma.newTree( - newIdentNode("used") - ) - ), - newEmptyNode(), - raisesTuple - ) - ), - prc.body - ) + if raw: # raw async = body is left as-is + if raises != nil and prc.kind notin {nnkProcTy, nnkLambda} and not isEmpty(prc.body): + # Inject `raises` type marker that causes `newFuture` to return a raise- + # tracking future instead of an ordinary future: + # + # type InternalRaisesFutureRaises = `raisesTuple` + # `body` + prc.body = nnkStmtList.newTree( + nnkTypeSection.newTree( + nnkTypeDef.newTree( + nnkPragmaExpr.newTree( + ident"InternalRaisesFutureRaises", + nnkPragma.newTree(ident "used")), + newEmptyNode(), + raises, + ) + ), + prc.body + ) + + when chronosDumpAsync: + echo repr prc return prc @@ -311,9 +324,6 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = setResultSym = ident "setResult" procBody = prc.body.processBody(setResultSym, baseType) internalFutureSym = ident "chronosInternalRetFuture" - internalFutureType = - if baseTypeIsVoid: futureVoidType - else: returnType castFutureSym = nnkCast.newTree(internalFutureType, internalFutureSym) resultIdent = ident "result" @@ -396,7 +406,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = castFutureSym, baseType, if baseTypeIsVoid: procBody # shortcut for non-generic `void` else: newCall(setResultSym, procBody), - raisesTuple + raises ) closureBody = newStmtList(resultDecl, setResultDecl, completeDecl) @@ -431,19 +441,22 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = outerProcBody.add(closureIterator) - # -> let resultFuture = newInternalRaisesFuture[T]() + # -> let resultFuture = newInternalRaisesFuture[T, E]() # declared at the end to be sure that the closure # doesn't reference it, avoid cyclic ref (#203) let retFutureSym = ident "resultFuture" + newFutProc = if raises == nil: + newTree(nnkBracketExpr, ident "newFuture", baseType) + else: + newTree(nnkBracketExpr, ident "newInternalRaisesFuture", baseType, raises) retFutureSym.copyLineInfo(prc) # Do not change this code to `quote do` version because `instantiationInfo` # will be broken for `newFuture()` call. outerProcBody.add( newLetStmt( retFutureSym, - newCall(newTree(nnkBracketExpr, ident "newInternalRaisesFuture", baseType), - newLit(prcName)) + newCall(newFutProc, newLit(prcName)) ) ) # -> resultFuture.internalClosure = iterator @@ -465,6 +478,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = when chronosDumpAsync: echo repr prc + prc template await*[T](f: Future[T]): untyped = @@ -490,32 +504,23 @@ template awaitne*[T](f: Future[T]): Future[T] = else: unsupported "awaitne is only available within {.async.}" -macro async*(prc: untyped): untyped = +macro async*(params, 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: - oneProc.addPragma(ident"async") - result.add asyncSingleProc(oneProc) + result.add asyncSingleProc(oneProc, params) else: - prc.addPragma(ident"async") - result = asyncSingleProc(prc) + result = asyncSingleProc(prc, params) + +macro async*(prc: untyped): untyped = + ## Macro which processes async procedures into the appropriate + ## iterators and yield statements. -macro asyncraises*(possibleExceptions, prc: untyped): untyped = - # Add back the pragma and let asyncSingleProc handle it - # Exerimental / subject to change and/or removal if prc.kind == nnkStmtList: result = newStmtList() for oneProc in prc: - oneProc.addPragma(nnkExprColonExpr.newTree( - ident"asyncraises", - possibleExceptions - )) - result.add asyncSingleProc(oneProc) + result.add asyncSingleProc(oneProc, nnkTupleConstr.newTree()) else: - prc.addPragma(nnkExprColonExpr.newTree( - ident"asyncraises", - possibleExceptions - )) - result = asyncSingleProc(prc) + result = asyncSingleProc(prc, nnkTupleConstr.newTree()) diff --git a/chronos/internal/raisesfutures.nim b/chronos/internal/raisesfutures.nim index 6a85581..ad811f7 100644 --- a/chronos/internal/raisesfutures.nim +++ b/chronos/internal/raisesfutures.nim @@ -7,13 +7,23 @@ type ## Future with a tuple of possible exception types ## eg InternalRaisesFuture[void, (ValueError, OSError)] ## - ## This type gets injected by `asyncraises` and similar utilities and - ## should not be used manually as the internal exception representation is - ## subject to change in future chronos versions. + ## This type gets injected by `async: (raises: ...)` and similar utilities + ## and should not be used manually as the internal exception representation + ## is subject to change in future chronos versions. + +proc makeNoRaises*(): NimNode {.compileTime.} = + # An empty tuple would have been easier but... + # https://github.com/nim-lang/Nim/issues/22863 + # https://github.com/nim-lang/Nim/issues/22865 + + ident"void" + +proc isNoRaises*(n: NimNode): bool {.compileTime.} = + n.eqIdent("void") iterator members(tup: NimNode): NimNode = # Given a typedesc[tuple] = (A, B, C), yields the tuple members (A, B C) - if not tup.eqIdent("void"): + if not isNoRaises(tup): for n in getType(getTypeInst(tup)[1])[1..^1]: yield n @@ -40,7 +50,7 @@ macro prepend*(tup: typedesc[tuple], typs: varargs[typed]): typedesc = result.add err if result.len == 0: - result = ident"void" + result = makeNoRaises() macro remove*(tup: typedesc[tuple], typs: varargs[typed]): typedesc = result = nnkTupleConstr.newTree() @@ -49,7 +59,7 @@ macro remove*(tup: typedesc[tuple], typs: varargs[typed]): typedesc = result.add err if result.len == 0: - result = ident"void" + result = makeNoRaises() macro union*(tup0: typedesc[tuple], tup1: typedesc[tuple]): typedesc = ## Join the types of the two tuples deduplicating the entries @@ -65,11 +75,13 @@ macro union*(tup0: typedesc[tuple], tup1: typedesc[tuple]): typedesc = for err2 in getType(getTypeInst(tup1)[1])[1..^1]: result.add err2 + if result.len == 0: + result = makeNoRaises() proc getRaises*(future: NimNode): NimNode {.compileTime.} = # Given InternalRaisesFuture[T, (A, B, C)], returns (A, B, C) let types = getType(getTypeInst(future)[2]) - if types.eqIdent("void"): + if isNoRaises(types): nnkBracketExpr.newTree(newEmptyNode()) else: expectKind(types, nnkBracketExpr) @@ -106,7 +118,7 @@ macro checkRaises*[T: CatchableError]( infix(error, "of", nnkBracketExpr.newTree(ident"typedesc", errorType))) let - errorMsg = "`fail`: `" & repr(toMatch) & "` incompatible with `asyncraises: " & repr(raises[1..^1]) & "`" + errorMsg = "`fail`: `" & repr(toMatch) & "` incompatible with `raises: " & repr(raises[1..^1]) & "`" warningMsg = "Can't verify `fail` exception type at compile time - expected one of " & repr(raises[1..^1]) & ", got `" & repr(toMatch) & "`" # A warning from this line means exception type will be verified at runtime warning = if warn: diff --git a/tests/testmacro.nim b/tests/testmacro.nim index 2fc24be..c9b45dd 100644 --- a/tests/testmacro.nim +++ b/tests/testmacro.nim @@ -387,16 +387,16 @@ suite "Exceptions tracking": check (not compiles(body)) test "Can raise valid exception": proc test1 {.async.} = raise newException(ValueError, "hey") - proc test2 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") - proc test3 {.async, asyncraises: [IOError, ValueError].} = + 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, asyncraises: [], used.} = raise newException(Defect, "hey") - proc test5 {.async, asyncraises: [].} = discard - proc test6 {.async, asyncraises: [].} = await test5() + proc test4 {.async: (raises: []), used.} = raise newException(Defect, "hey") + proc test5 {.async: (raises: []).} = discard + proc test6 {.async: (raises: []).} = await test5() expect(ValueError): waitFor test1() expect(ValueError): waitFor test2() @@ -405,15 +405,15 @@ suite "Exceptions tracking": test "Cannot raise invalid exception": checkNotCompiles: - proc test3 {.async, asyncraises: [IOError].} = raise newException(ValueError, "hey") + proc test3 {.async: (raises: [IOError]).} = raise newException(ValueError, "hey") test "Explicit return in non-raising proc": - proc test(): Future[int] {.async, asyncraises: [].} = return 12 + proc test(): Future[int] {.async: (raises: []).} = return 12 check: waitFor(test()) == 12 test "Non-raising compatibility": - proc test1 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") + proc test1 {.async: (raises: [ValueError]).} = raise newException(ValueError, "hey") let testVar: Future[void] = test1() proc test2 {.async.} = raise newException(ValueError, "hey") @@ -423,69 +423,64 @@ suite "Exceptions tracking": #let testVar3: proc: Future[void] = test1 test "Cannot store invalid future types": - proc test1 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") - proc test2 {.async, asyncraises: [IOError].} = raise newException(IOError, "hey") + proc test1 {.async: (raises: [ValueError]).} = raise newException(ValueError, "hey") + proc test2 {.async: (raises: [IOError]).} = raise newException(IOError, "hey") var a = test1() checkNotCompiles: a = test2() test "Await raises the correct types": - proc test1 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") - proc test2 {.async, asyncraises: [ValueError, CancelledError].} = await test1() + proc test1 {.async: (raises: [ValueError]).} = raise newException(ValueError, "hey") + proc test2 {.async: (raises: [ValueError, CancelledError]).} = await test1() checkNotCompiles: - proc test3 {.async, asyncraises: [CancelledError].} = await test1() + proc test3 {.async: (raises: [CancelledError]).} = await test1() test "Can create callbacks": - proc test1 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") - let callback: proc() {.async, asyncraises: [ValueError].} = test1 + proc test1 {.async: (raises: [ValueError]).} = raise newException(ValueError, "hey") + let callback: proc() {.async: (raises: [ValueError]).} = test1 test "Can return values": - proc test1: Future[int] {.async, asyncraises: [ValueError].} = + proc test1: Future[int] {.async: (raises: [ValueError]).} = if 1 == 0: raise newException(ValueError, "hey") return 12 - proc test2: Future[int] {.async, asyncraises: [ValueError, IOError, CancelledError].} = + proc test2: Future[int] {.async: (raises: [ValueError, IOError, CancelledError]).} = return await test1() checkNotCompiles: - proc test3: Future[int] {.async, asyncraises: [CancelledError].} = await test1() + proc test3: Future[int] {.async: (raises: [CancelledError]).} = await test1() check waitFor(test2()) == 12 test "Manual tracking": - proc test1: Future[int] {.asyncraises: [ValueError].} = + proc test1: Future[int] {.async: (raw: true, raises: [ValueError]).} = result = newFuture[int]() result.complete(12) check waitFor(test1()) == 12 - proc test2: Future[int] {.asyncraises: [IOError, OSError].} = + proc test2: Future[int] {.async: (raw: true, raises: [IOError, OSError]).} = result = newFuture[int]() result.fail(newException(IOError, "fail")) result.fail(newException(OSError, "fail")) checkNotCompiles: result.fail(newException(ValueError, "fail")) - proc test3: Future[void] {.asyncraises: [].} = + proc test3: Future[void] {.async: (raw: true, raises: []).} = checkNotCompiles: result.fail(newException(ValueError, "fail")) # Inheritance - proc test4: Future[void] {.asyncraises: [CatchableError].} = + proc test4: Future[void] {.async: (raw: true, raises: [CatchableError]).} = result.fail(newException(IOError, "fail")) - test "Reversed async, asyncraises": - proc test44 {.asyncraises: [ValueError], async.} = raise newException(ValueError, "hey") - checkNotCompiles: - proc test33 {.asyncraises: [IOError], async.} = raise newException(ValueError, "hey") - test "or errors": - proc testit {.asyncraises: [ValueError], async.} = + proc testit {.async: (raises: [ValueError]).} = raise (ref ValueError)() - proc testit2 {.asyncraises: [IOError], async.} = + proc testit2 {.async: (raises: [IOError]).} = raise (ref IOError)() - proc test {.async, asyncraises: [ValueError, IOError].} = + proc test {.async: (raises: [ValueError, IOError]).} = await testit() or testit2() proc noraises() {.raises: [].} = @@ -499,9 +494,10 @@ suite "Exceptions tracking": noraises() test "Wait errors": - proc testit {.asyncraises: [ValueError], async.} = raise newException(ValueError, "hey") + proc testit {.async: (raises: [ValueError]).} = + raise newException(ValueError, "hey") - proc test {.async, asyncraises: [ValueError, AsyncTimeoutError, CancelledError].} = + proc test {.async: (raises: [ValueError, AsyncTimeoutError, CancelledError]).} = await wait(testit(), 1000.milliseconds) proc noraises() {.raises: [].} = @@ -513,11 +509,11 @@ suite "Exceptions tracking": noraises() test "Nocancel errors": - proc testit {.asyncraises: [ValueError, CancelledError], async.} = + proc testit {.async: (raises: [ValueError, CancelledError]).} = await sleepAsync(5.milliseconds) raise (ref ValueError)() - proc test {.async, asyncraises: [ValueError].} = + proc test {.async: (raises: [ValueError]).} = await noCancel testit() proc noraises() {.raises: [].} =