`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, ...).}`
```
This commit is contained in:
Jacek Sieka 2023-11-07 11:12:59 +01:00 committed by GitHub
parent be2edab3ac
commit cd6369c048
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 239 additions and 205 deletions

110
README.md
View File

@ -46,18 +46,18 @@ Submit a PR to add yours!
### Concepts ### Concepts
Chronos implements the async/await paradigm in a self-contained library, using Chronos implements the async/await paradigm in a self-contained library using
macros, with no specific helpers from the compiler. 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. created, as soon as one is needed.
To trigger a dispatcher's processing step, we need to call `poll()` - either 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 handles any file descriptors, timers and callbacks that are ready to be
processed. 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, and a list of callbacks to be scheduled after any type of
completion - be that success, failure or cancellation. 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 and placed in the `Future.error` field, changing the future's status to
`Failed`. `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 by a hidden `try` block in the calling async procedure. That's how these
exceptions move up the async chain. exceptions move up the async chain.
@ -214,57 +214,81 @@ by the transformation.
#### Checked exceptions #### Checked exceptions
By specifying a `asyncraises` list to an async procedure, you can check which By specifying a `raises` list to an async procedure, you can check which
exceptions can be thrown by it. exceptions can be raised by it:
```nim ```nim
proc p1(): Future[void] {.async, asyncraises: [IOError].} = proc p1(): Future[void] {.async: (raises: [IOError]).} =
assert not (compiles do: raise newException(ValueError, "uh-uh")) assert not (compiles do: raise newException(ValueError, "uh-uh"))
raise newException(IOError, "works") # Or any child of IOError raise newException(IOError, "works") # Or any child of IOError
```
Under the hood, the return type of `p1` will be rewritten to an internal type, proc p2(): Future[void] {.async, (raises: [IOError]).} =
which will convey raises informations to `await`.
```nim
proc p2(): Future[void] {.async, asyncraises: [IOError].} =
await p1() # Works, because await knows that p1 await p1() # Works, because await knows that p1
# can only raise IOError # can only raise IOError
``` ```
Raw functions and callbacks that don't go through the `async` transformation but Under the hood, the return type of `p1` will be rewritten to an internal type
still return a `Future` and interact with the rest of the framework also need to which will convey raises informations to `await`.
be annotated with `asyncraises` to participate in the checked exception scheme:
### 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 ```nim
proc p3(): Future[void] {.async, asyncraises: [IOError].} = proc rawAsync(): Future[void] {.async: (raw: true).} =
let fut: Future[void] = p1() # works let future = newFuture[void]("rawAsync")
assert not compiles(await fut) # await lost informations about raises, future.complete()
# so it can raise anything return future
# 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_
)
``` ```
When `chronos` performs the `async` transformation, all code is placed in a Raw functions must not raise exceptions directly - they are implicitly declared
a special `try/except` clause that re-routes exception handling to the `Future`. as `raises: []` - instead they should store exceptions in the returned `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:
```nim ```nim
proc p4(): Future[void] {.asyncraises: [ValueError].} = proc rawFailure(): Future[void] {.async: (raw: true).} =
let fut = newFuture[void] let future = newFuture[void]("rawAsync")
future.fail((ref ValueError)(msg: "Oh no!"))
return future
```
# Equivalent of `raise (ref ValueError)()` in raw async functions: Raw functions can also use checked exceptions:
fut.fail((ref ValueError)(msg: "raising in raw async function"))
fut ```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 ### 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 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. 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` `chronos` currently offers minimal support for exception effects and `raises`
annotations. In general, during the `async` transformation, a generic annotations. In general, during the `async` transformation, a generic

View File

@ -131,4 +131,4 @@
import ./internal/[asyncengine, asyncfutures, asyncmacro, errors] import ./internal/[asyncengine, asyncfutures, asyncmacro, errors]
export asyncfutures, asyncengine, errors export asyncfutures, asyncengine, errors
export asyncmacro.async, asyncmacro.await, asyncmacro.awaitne, asyncraises export asyncmacro.async, asyncmacro.await, asyncmacro.awaitne

View File

@ -21,7 +21,7 @@ export Port
export deques, errors, futures, timer, results export deques, errors, futures, timer, results
export export
asyncmacro.async, asyncmacro.await, asyncmacro.awaitne, asyncmacro.asyncraises asyncmacro.async, asyncmacro.await, asyncmacro.awaitne
const const
MaxEventsCount* = 64 MaxEventsCount* = 64

View File

@ -102,18 +102,12 @@ template newFuture*[T](fromProc: static[string] = "",
else: else:
newFutureImpl[T](getSrcLocation(fromProc), flags) newFutureImpl[T](getSrcLocation(fromProc), flags)
macro getFutureExceptions(T: typedesc): untyped = template newInternalRaisesFuture*[T, E](fromProc: static[string] = ""): auto =
if getTypeInst(T)[1].len > 2:
getTypeInst(T)[1][2]
else:
ident"void"
template newInternalRaisesFuture*[T](fromProc: static[string] = ""): auto =
## Creates a new future. ## Creates a new future.
## ##
## Specifying ``fromProc``, which is a string specifying the name of the proc ## 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. ## 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] = template newFutureSeq*[A, B](fromProc: static[string] = ""): FutureSeq[A, B] =
## Create a new future which can hold/preserve GC sequence until future will ## 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 e = getTypeInst(f)[2]
let types = getType(e) let types = getType(e)
if types.eqIdent("void"): if isNoRaises(types):
return quote do: return quote do:
if not(isNil(`f`.internalError)): if not(isNil(`f`.internalError)):
raiseAssert("Unhandled future exception: " & `f`.error.msg) raiseAssert("Unhandled future exception: " & `f`.error.msg)
@ -484,7 +478,6 @@ macro internalCheckComplete*(f: InternalRaisesFuture): untyped =
expectKind(types, nnkBracketExpr) expectKind(types, nnkBracketExpr)
expectKind(types[0], nnkSym) expectKind(types[0], nnkSym)
assert types[0].strVal == "tuple" assert types[0].strVal == "tuple"
assert types.len > 1
let ifRaise = nnkIfExpr.newTree( let ifRaise = nnkIfExpr.newTree(
nnkElifExpr.newTree( nnkElifExpr.newTree(
@ -914,7 +907,7 @@ template cancel*(future: FutureBase) {.
cancelSoon(future, nil, nil, getSrcLocation()) cancelSoon(future, nil, nil, getSrcLocation())
proc cancelAndWait*(future: FutureBase, loc: ptr SrcLoc): Future[void] {. 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 ## Perform cancellation ``future`` return Future which will be completed when
## ``future`` become finished (completed with value, failed or cancelled). ## ``future`` become finished (completed with value, failed or cancelled).
## ##
@ -938,7 +931,7 @@ template cancelAndWait*(future: FutureBase): Future[void] =
## Cancel ``future``. ## Cancel ``future``.
cancelAndWait(future, getSrcLocation()) 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 ## Prevent cancellation requests from propagating to ``future`` while
## forwarding its value or error when it finishes. ## forwarding its value or error when it finishes.
## ##
@ -978,7 +971,7 @@ proc noCancel*[F: SomeFuture](future: F): auto = # asyncraises: asyncraiseOf(fut
retFuture retFuture
proc allFutures*(futs: varargs[FutureBase]): Future[void] {. 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`` ## Returns a future which will complete only when all futures in ``futs``
## will be completed, failed or canceled. ## will be completed, failed or canceled.
## ##
@ -1017,7 +1010,7 @@ proc allFutures*(futs: varargs[FutureBase]): Future[void] {.
retFuture retFuture
proc allFutures*[T](futs: varargs[Future[T]]): Future[void] {. 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`` ## Returns a future which will complete only when all futures in ``futs``
## will be completed, failed or canceled. ## will be completed, failed or canceled.
## ##
@ -1031,7 +1024,7 @@ proc allFutures*[T](futs: varargs[Future[T]]): Future[void] {.
allFutures(nfuts) allFutures(nfuts)
proc allFinished*[F: SomeFuture](futs: varargs[F]): Future[seq[F]] {. 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`` ## Returns a future which will complete only when all futures in ``futs``
## will be completed, failed or canceled. ## will be completed, failed or canceled.
## ##
@ -1072,7 +1065,7 @@ proc allFinished*[F: SomeFuture](futs: varargs[F]): Future[seq[F]] {.
return retFuture return retFuture
proc one*[F: SomeFuture](futs: varargs[F]): Future[F] {. 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, ## 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. ## 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 return retFuture
proc race*(futs: varargs[FutureBase]): Future[FutureBase] {. proc race*(futs: varargs[FutureBase]): Future[FutureBase] {.
asyncraises: [CancelledError].} = async: (raw: true, raises: [CancelledError]).} =
## Returns a future which will complete and return completed FutureBase, ## Returns a future which will complete and return completed FutureBase,
## when one of the futures in ``futs`` will be completed, failed or canceled. ## 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): when (chronosEventEngine in ["epoll", "kqueue"]) or defined(windows):
import std/os 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 retFuture = newFuture[void]("chronos.waitSignal()")
var signalHandle: Opt[SignalHandle] var signalHandle: Opt[SignalHandle]
@ -1208,7 +1202,7 @@ when (chronosEventEngine in ["epoll", "kqueue"]) or defined(windows):
retFuture retFuture
proc sleepAsync*(duration: Duration): Future[void] {. proc sleepAsync*(duration: Duration): Future[void] {.
asyncraises: [CancelledError].} = async: (raw: true, raises: [CancelledError]).} =
## Suspends the execution of the current async procedure for the next ## Suspends the execution of the current async procedure for the next
## ``duration`` time. ## ``duration`` time.
var retFuture = newFuture[void]("chronos.sleepAsync(Duration)") var retFuture = newFuture[void]("chronos.sleepAsync(Duration)")
@ -1228,10 +1222,12 @@ proc sleepAsync*(duration: Duration): Future[void] {.
return retFuture return retFuture
proc sleepAsync*(ms: int): Future[void] {. 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()) 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 ## Suspends the execution of the current async procedure for the next
## ``number`` of asynchronous steps (``poll()`` calls). ## ``number`` of asynchronous steps (``poll()`` calls).
## ##
@ -1258,7 +1254,8 @@ proc stepsAsync*(number: int): Future[void] {.asyncraises: [CancelledError].} =
retFuture 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. ## Suspends the execution of the current asynchronous task until "idle" time.
## ##
## "idle" time its moment of time, when no network events were processed by ## "idle" time its moment of time, when no network events were processed by
@ -1277,7 +1274,7 @@ proc idleAsync*(): Future[void] {.asyncraises: [CancelledError].} =
retFuture retFuture
proc withTimeout*[T](fut: Future[T], timeout: Duration): Future[bool] {. 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 ## Returns a future which will complete once ``fut`` completes or after
## ``timeout`` milliseconds has elapsed. ## ``timeout`` milliseconds has elapsed.
## ##

View File

@ -9,8 +9,9 @@
# #
import import
std/[algorithm, macros, sequtils], std/[macros],
../[futures, config] ../[futures, config],
./raisesfutures
proc processBody(node, setResultSym, baseType: NimNode): NimNode {.compileTime.} = proc processBody(node, setResultSym, baseType: NimNode): NimNode {.compileTime.} =
case node.kind case node.kind
@ -32,10 +33,10 @@ proc processBody(node, setResultSym, baseType: NimNode): NimNode {.compileTime.}
node[i] = processBody(node[i], setResultSym, baseType) node[i] = processBody(node[i], setResultSym, baseType)
node node
proc wrapInTryFinally(fut, baseType, body, raisesTuple: NimNode): NimNode {.compileTime.} = proc wrapInTryFinally(fut, baseType, body, raises: NimNode): NimNode {.compileTime.} =
# creates: # creates:
# try: `body` # try: `body`
# [for raise in raisesTuple]: # [for raise in raises]:
# except `raise`: closureSucceeded = false; `castFutureSym`.fail(exc) # except `raise`: closureSucceeded = false; `castFutureSym`.fail(exc)
# finally: # finally:
# if closureSucceeded: # if closureSucceeded:
@ -91,7 +92,17 @@ proc wrapInTryFinally(fut, baseType, body, raisesTuple: NimNode): NimNode {.comp
newCall(ident "fail", fut, excName) 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"): if exc.eqIdent("Exception"):
addCancelledError addCancelledError
addCatchableError addCatchableError
@ -182,42 +193,33 @@ proc cleanupOpenSymChoice(node: NimNode): NimNode {.compileTime.} =
for child in node: for child in node:
result.add(cleanupOpenSymChoice(child)) result.add(cleanupOpenSymChoice(child))
proc getAsyncCfg(prc: NimNode): tuple[raises: bool, async: bool, raisesTuple: NimNode] = proc decodeParams(params: NimNode): tuple[raw: bool, raises: NimNode] =
# reads the pragmas to extract the useful data # decodes the parameter tuple given in `async: (name: value, ...)` to its
# and removes them # recognised parts
params.expectKind(nnkTupleConstr)
var var
foundRaises = -1 raw = false
foundAsync = -1 raises: NimNode = nil
for index, pragma in pragma(prc): for param in params:
if pragma.kind == nnkExprColonExpr and pragma[0] == ident "asyncraises": param.expectKind(nnkExprColonExpr)
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")
result.raises = foundRaises >= 0 if param[0].eqIdent("raises"):
result.async = foundAsync >= 0 param[1].expectKind(nnkBracket)
result.raisesTuple = nnkTupleConstr.newTree() 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: (raw, raises)
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)
proc isEmpty(n: NimNode): bool {.compileTime.} = proc isEmpty(n: NimNode): bool {.compileTime.} =
# true iff node recursively contains only comments or empties # true iff node recursively contains only comments or empties
@ -230,13 +232,18 @@ proc isEmpty(n: NimNode): bool {.compileTime.} =
else: else:
false false
proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = proc asyncSingleProc(prc, params: NimNode): NimNode {.compileTime.} =
## This macro transforms a single procedure into a closure iterator. ## This macro transforms a single procedure into a closure iterator.
## The ``async`` macro supports a stmtList holding multiple async procedures. ## The ``async`` macro supports a stmtList holding multiple async procedures.
if prc.kind notin {nnkProcTy, nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}: if prc.kind notin {nnkProcTy, nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
error("Cannot transform " & $prc.kind & " into an async proc." & error("Cannot transform " & $prc.kind & " into an async proc." &
" proc/method definition or lambda node expected.", prc) " 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]) let returnType = cleanupOpenSymChoice(prc.params2[0])
# Verify that the return type is a Future[T] # Verify that the return type is a Future[T]
@ -254,22 +261,24 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
let let
baseTypeIsVoid = baseType.eqIdent("void") baseTypeIsVoid = baseType.eqIdent("void")
futureVoidType = nnkBracketExpr.newTree(ident "Future", ident "void") (raw, raises) = decodeParams(params)
(hasRaises, isAsync, raisesTuple) = getAsyncCfg(prc) internalFutureType =
if baseTypeIsVoid:
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] =
newNimNode(nnkBracketExpr, prc). newNimNode(nnkBracketExpr, prc).
add(newIdentNode("Future")). 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? if prc.kind notin {nnkProcTy, nnkLambda}: # TODO: Nim bug?
prc.addPragma(newColonExpr(ident "stackTrace", ident "off")) 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 # https://github.com/nim-lang/RFCs/issues/435
prc.addPragma(newIdentNode("gcsafe")) prc.addPragma(newIdentNode("gcsafe"))
if isAsync == false: # `asyncraises` without `async` if raw: # raw async = body is left as-is
# type InternalRaisesFutureRaises {.used.} = `raisesTuple` if raises != nil and prc.kind notin {nnkProcTy, nnkLambda} and not isEmpty(prc.body):
# `body` # Inject `raises` type marker that causes `newFuture` to return a raise-
prc.body = nnkStmtList.newTree( # tracking future instead of an ordinary future:
nnkTypeSection.newTree( #
nnkTypeDef.newTree( # type InternalRaisesFutureRaises = `raisesTuple`
nnkPragmaExpr.newTree( # `body`
ident"InternalRaisesFutureRaises", prc.body = nnkStmtList.newTree(
nnkPragma.newTree( nnkTypeSection.newTree(
newIdentNode("used") nnkTypeDef.newTree(
) nnkPragmaExpr.newTree(
), ident"InternalRaisesFutureRaises",
newEmptyNode(), nnkPragma.newTree(ident "used")),
raisesTuple newEmptyNode(),
) raises,
), )
prc.body ),
) prc.body
)
when chronosDumpAsync:
echo repr prc
return prc return prc
@ -311,9 +324,6 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
setResultSym = ident "setResult" setResultSym = ident "setResult"
procBody = prc.body.processBody(setResultSym, baseType) procBody = prc.body.processBody(setResultSym, baseType)
internalFutureSym = ident "chronosInternalRetFuture" internalFutureSym = ident "chronosInternalRetFuture"
internalFutureType =
if baseTypeIsVoid: futureVoidType
else: returnType
castFutureSym = nnkCast.newTree(internalFutureType, internalFutureSym) castFutureSym = nnkCast.newTree(internalFutureType, internalFutureSym)
resultIdent = ident "result" resultIdent = ident "result"
@ -396,7 +406,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
castFutureSym, baseType, castFutureSym, baseType,
if baseTypeIsVoid: procBody # shortcut for non-generic `void` if baseTypeIsVoid: procBody # shortcut for non-generic `void`
else: newCall(setResultSym, procBody), else: newCall(setResultSym, procBody),
raisesTuple raises
) )
closureBody = newStmtList(resultDecl, setResultDecl, completeDecl) closureBody = newStmtList(resultDecl, setResultDecl, completeDecl)
@ -431,19 +441,22 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
outerProcBody.add(closureIterator) outerProcBody.add(closureIterator)
# -> let resultFuture = newInternalRaisesFuture[T]() # -> let resultFuture = newInternalRaisesFuture[T, E]()
# declared at the end to be sure that the closure # declared at the end to be sure that the closure
# doesn't reference it, avoid cyclic ref (#203) # doesn't reference it, avoid cyclic ref (#203)
let let
retFutureSym = ident "resultFuture" retFutureSym = ident "resultFuture"
newFutProc = if raises == nil:
newTree(nnkBracketExpr, ident "newFuture", baseType)
else:
newTree(nnkBracketExpr, ident "newInternalRaisesFuture", baseType, raises)
retFutureSym.copyLineInfo(prc) retFutureSym.copyLineInfo(prc)
# Do not change this code to `quote do` version because `instantiationInfo` # Do not change this code to `quote do` version because `instantiationInfo`
# will be broken for `newFuture()` call. # will be broken for `newFuture()` call.
outerProcBody.add( outerProcBody.add(
newLetStmt( newLetStmt(
retFutureSym, retFutureSym,
newCall(newTree(nnkBracketExpr, ident "newInternalRaisesFuture", baseType), newCall(newFutProc, newLit(prcName))
newLit(prcName))
) )
) )
# -> resultFuture.internalClosure = iterator # -> resultFuture.internalClosure = iterator
@ -465,6 +478,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
when chronosDumpAsync: when chronosDumpAsync:
echo repr prc echo repr prc
prc prc
template await*[T](f: Future[T]): untyped = template await*[T](f: Future[T]): untyped =
@ -490,32 +504,23 @@ template awaitne*[T](f: Future[T]): Future[T] =
else: else:
unsupported "awaitne is only available within {.async.}" 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 ## Macro which processes async procedures into the appropriate
## iterators and yield statements. ## iterators and yield statements.
if prc.kind == nnkStmtList: if prc.kind == nnkStmtList:
result = newStmtList() result = newStmtList()
for oneProc in prc: for oneProc in prc:
oneProc.addPragma(ident"async") result.add asyncSingleProc(oneProc, params)
result.add asyncSingleProc(oneProc)
else: else:
prc.addPragma(ident"async") result = asyncSingleProc(prc, params)
result = asyncSingleProc(prc)
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: if prc.kind == nnkStmtList:
result = newStmtList() result = newStmtList()
for oneProc in prc: for oneProc in prc:
oneProc.addPragma(nnkExprColonExpr.newTree( result.add asyncSingleProc(oneProc, nnkTupleConstr.newTree())
ident"asyncraises",
possibleExceptions
))
result.add asyncSingleProc(oneProc)
else: else:
prc.addPragma(nnkExprColonExpr.newTree( result = asyncSingleProc(prc, nnkTupleConstr.newTree())
ident"asyncraises",
possibleExceptions
))
result = asyncSingleProc(prc)

View File

@ -7,13 +7,23 @@ type
## Future with a tuple of possible exception types ## Future with a tuple of possible exception types
## eg InternalRaisesFuture[void, (ValueError, OSError)] ## eg InternalRaisesFuture[void, (ValueError, OSError)]
## ##
## This type gets injected by `asyncraises` and similar utilities and ## This type gets injected by `async: (raises: ...)` and similar utilities
## should not be used manually as the internal exception representation is ## and should not be used manually as the internal exception representation
## subject to change in future chronos versions. ## 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 = iterator members(tup: NimNode): NimNode =
# Given a typedesc[tuple] = (A, B, C), yields the tuple members (A, B C) # 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]: for n in getType(getTypeInst(tup)[1])[1..^1]:
yield n yield n
@ -40,7 +50,7 @@ macro prepend*(tup: typedesc[tuple], typs: varargs[typed]): typedesc =
result.add err result.add err
if result.len == 0: if result.len == 0:
result = ident"void" result = makeNoRaises()
macro remove*(tup: typedesc[tuple], typs: varargs[typed]): typedesc = macro remove*(tup: typedesc[tuple], typs: varargs[typed]): typedesc =
result = nnkTupleConstr.newTree() result = nnkTupleConstr.newTree()
@ -49,7 +59,7 @@ macro remove*(tup: typedesc[tuple], typs: varargs[typed]): typedesc =
result.add err result.add err
if result.len == 0: if result.len == 0:
result = ident"void" result = makeNoRaises()
macro union*(tup0: typedesc[tuple], tup1: typedesc[tuple]): typedesc = macro union*(tup0: typedesc[tuple], tup1: typedesc[tuple]): typedesc =
## Join the types of the two tuples deduplicating the entries ## 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]: for err2 in getType(getTypeInst(tup1)[1])[1..^1]:
result.add err2 result.add err2
if result.len == 0:
result = makeNoRaises()
proc getRaises*(future: NimNode): NimNode {.compileTime.} = proc getRaises*(future: NimNode): NimNode {.compileTime.} =
# Given InternalRaisesFuture[T, (A, B, C)], returns (A, B, C) # Given InternalRaisesFuture[T, (A, B, C)], returns (A, B, C)
let types = getType(getTypeInst(future)[2]) let types = getType(getTypeInst(future)[2])
if types.eqIdent("void"): if isNoRaises(types):
nnkBracketExpr.newTree(newEmptyNode()) nnkBracketExpr.newTree(newEmptyNode())
else: else:
expectKind(types, nnkBracketExpr) expectKind(types, nnkBracketExpr)
@ -106,7 +118,7 @@ macro checkRaises*[T: CatchableError](
infix(error, "of", nnkBracketExpr.newTree(ident"typedesc", errorType))) infix(error, "of", nnkBracketExpr.newTree(ident"typedesc", errorType)))
let 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) & "`" 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 # A warning from this line means exception type will be verified at runtime
warning = if warn: warning = if warn:

View File

@ -387,16 +387,16 @@ suite "Exceptions tracking":
check (not compiles(body)) check (not compiles(body))
test "Can raise valid exception": test "Can raise valid exception":
proc test1 {.async.} = raise newException(ValueError, "hey") proc test1 {.async.} = raise newException(ValueError, "hey")
proc test2 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") proc test2 {.async: (raises: [ValueError]).} = raise newException(ValueError, "hey")
proc test3 {.async, asyncraises: [IOError, ValueError].} = proc test3 {.async: (raises: [IOError, ValueError]).} =
if 1 == 2: if 1 == 2:
raise newException(ValueError, "hey") raise newException(ValueError, "hey")
else: else:
raise newException(IOError, "hey") raise newException(IOError, "hey")
proc test4 {.async, asyncraises: [], used.} = raise newException(Defect, "hey") proc test4 {.async: (raises: []), used.} = raise newException(Defect, "hey")
proc test5 {.async, asyncraises: [].} = discard proc test5 {.async: (raises: []).} = discard
proc test6 {.async, asyncraises: [].} = await test5() proc test6 {.async: (raises: []).} = await test5()
expect(ValueError): waitFor test1() expect(ValueError): waitFor test1()
expect(ValueError): waitFor test2() expect(ValueError): waitFor test2()
@ -405,15 +405,15 @@ suite "Exceptions tracking":
test "Cannot raise invalid exception": test "Cannot raise invalid exception":
checkNotCompiles: 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": test "Explicit return in non-raising proc":
proc test(): Future[int] {.async, asyncraises: [].} = return 12 proc test(): Future[int] {.async: (raises: []).} = return 12
check: check:
waitFor(test()) == 12 waitFor(test()) == 12
test "Non-raising compatibility": 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() let testVar: Future[void] = test1()
proc test2 {.async.} = raise newException(ValueError, "hey") proc test2 {.async.} = raise newException(ValueError, "hey")
@ -423,69 +423,64 @@ suite "Exceptions tracking":
#let testVar3: proc: Future[void] = test1 #let testVar3: proc: Future[void] = test1
test "Cannot store invalid future types": test "Cannot store invalid future types":
proc test1 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") proc test1 {.async: (raises: [ValueError]).} = raise newException(ValueError, "hey")
proc test2 {.async, asyncraises: [IOError].} = raise newException(IOError, "hey") proc test2 {.async: (raises: [IOError]).} = raise newException(IOError, "hey")
var a = test1() var a = test1()
checkNotCompiles: checkNotCompiles:
a = test2() a = test2()
test "Await raises the correct types": test "Await raises the correct types":
proc test1 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") proc test1 {.async: (raises: [ValueError]).} = raise newException(ValueError, "hey")
proc test2 {.async, asyncraises: [ValueError, CancelledError].} = await test1() proc test2 {.async: (raises: [ValueError, CancelledError]).} = await test1()
checkNotCompiles: checkNotCompiles:
proc test3 {.async, asyncraises: [CancelledError].} = await test1() proc test3 {.async: (raises: [CancelledError]).} = await test1()
test "Can create callbacks": test "Can create callbacks":
proc test1 {.async, asyncraises: [ValueError].} = raise newException(ValueError, "hey") proc test1 {.async: (raises: [ValueError]).} = raise newException(ValueError, "hey")
let callback: proc() {.async, asyncraises: [ValueError].} = test1 let callback: proc() {.async: (raises: [ValueError]).} = test1
test "Can return values": 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") if 1 == 0: raise newException(ValueError, "hey")
return 12 return 12
proc test2: Future[int] {.async, asyncraises: [ValueError, IOError, CancelledError].} = proc test2: Future[int] {.async: (raises: [ValueError, IOError, CancelledError]).} =
return await test1() return await test1()
checkNotCompiles: checkNotCompiles:
proc test3: Future[int] {.async, asyncraises: [CancelledError].} = await test1() proc test3: Future[int] {.async: (raises: [CancelledError]).} = await test1()
check waitFor(test2()) == 12 check waitFor(test2()) == 12
test "Manual tracking": test "Manual tracking":
proc test1: Future[int] {.asyncraises: [ValueError].} = proc test1: Future[int] {.async: (raw: true, raises: [ValueError]).} =
result = newFuture[int]() result = newFuture[int]()
result.complete(12) result.complete(12)
check waitFor(test1()) == 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 = newFuture[int]()
result.fail(newException(IOError, "fail")) result.fail(newException(IOError, "fail"))
result.fail(newException(OSError, "fail")) result.fail(newException(OSError, "fail"))
checkNotCompiles: checkNotCompiles:
result.fail(newException(ValueError, "fail")) result.fail(newException(ValueError, "fail"))
proc test3: Future[void] {.asyncraises: [].} = proc test3: Future[void] {.async: (raw: true, raises: []).} =
checkNotCompiles: checkNotCompiles:
result.fail(newException(ValueError, "fail")) result.fail(newException(ValueError, "fail"))
# Inheritance # Inheritance
proc test4: Future[void] {.asyncraises: [CatchableError].} = proc test4: Future[void] {.async: (raw: true, raises: [CatchableError]).} =
result.fail(newException(IOError, "fail")) 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": test "or errors":
proc testit {.asyncraises: [ValueError], async.} = proc testit {.async: (raises: [ValueError]).} =
raise (ref ValueError)() raise (ref ValueError)()
proc testit2 {.asyncraises: [IOError], async.} = proc testit2 {.async: (raises: [IOError]).} =
raise (ref IOError)() raise (ref IOError)()
proc test {.async, asyncraises: [ValueError, IOError].} = proc test {.async: (raises: [ValueError, IOError]).} =
await testit() or testit2() await testit() or testit2()
proc noraises() {.raises: [].} = proc noraises() {.raises: [].} =
@ -499,9 +494,10 @@ suite "Exceptions tracking":
noraises() noraises()
test "Wait errors": 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) await wait(testit(), 1000.milliseconds)
proc noraises() {.raises: [].} = proc noraises() {.raises: [].} =
@ -513,11 +509,11 @@ suite "Exceptions tracking":
noraises() noraises()
test "Nocancel errors": test "Nocancel errors":
proc testit {.asyncraises: [ValueError, CancelledError], async.} = proc testit {.async: (raises: [ValueError, CancelledError]).} =
await sleepAsync(5.milliseconds) await sleepAsync(5.milliseconds)
raise (ref ValueError)() raise (ref ValueError)()
proc test {.async, asyncraises: [ValueError].} = proc test {.async: (raises: [ValueError]).} =
await noCancel testit() await noCancel testit()
proc noraises() {.raises: [].} = proc noraises() {.raises: [].} =