`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
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

View File

@ -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

View File

@ -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

View File

@ -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.
##

View File

@ -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())

View File

@ -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:

View File

@ -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: [].} =