`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:
parent
be2edab3ac
commit
cd6369c048
110
README.md
110
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
##
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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: [].} =
|
||||
|
|
Loading…
Reference in New Issue