diff --git a/lib/pure/asyncfutures.nim b/lib/pure/asyncfutures.nim index 41f1f70a3752..c53c13437658 100644 --- a/lib/pure/asyncfutures.nim +++ b/lib/pure/asyncfutures.nim @@ -7,7 +7,7 @@ # distribution, for details about the copyright. # -import std/[os, sets, tables, strutils, times, heapqueue, options, deques, cstrutils, typetraits] +import std/[os, macros, sets, tables, strutils, strformat, times, heapqueue, options, deques, cstrutils, typetraits] import system/stacktraces @@ -39,6 +39,8 @@ type FutureVar*[T] = distinct Future[T] + FutureEx*[T, E] = distinct Future[T] + FutureError* = object of Defect cause*: FutureBase @@ -141,7 +143,7 @@ proc clean*[T](future: FutureVar[T]) = Future[T](future).finished = false Future[T](future).error = nil -proc checkFinished[T](future: Future[T]) = +proc checkFinished[T](future: Future[T]) {.raises: [].} = ## Checks whether `future` is finished. If it is then raises a ## `FutureError`. when not defined(release): @@ -162,11 +164,13 @@ proc checkFinished[T](future: Future[T]) = err.cause = future raise err -proc call(callbacks: var CallbackList) = +proc call(callbacks: var CallbackList) {.raises: [].} = var current = callbacks while true: if not current.function.isNil: - callSoon(current.function) + # XXX make callbacks {.raise: [].} + {.cast(raises: []).}: + callSoon(current.function) if current.next.isNil: break @@ -293,12 +297,11 @@ template getFilenameProcname(entry: StackTraceEntry): (string, string) = else: ($entry.filename, $entry.procname) -proc format(entry: StackTraceEntry): string = +proc format(entry: StackTraceEntry): string {.raises: [].} = let (filename, procname) = getFilenameProcname(entry) - let left = "$#($#)" % [filename, $entry.line] - result = spaces(2) & "$# $#\n" % [left, procname] + result = &"{spaces(2)}{filename}({$entry.line}) {procname}\n" -proc isInternal(entry: StackTraceEntry): bool = +proc isInternal(entry: StackTraceEntry): bool {.raises: [].} = # --excessiveStackTrace:off const internals = [ "asyncdispatch.nim", @@ -311,7 +314,7 @@ proc isInternal(entry: StackTraceEntry): bool = return true return false -proc `$`*(stackTraceEntries: seq[StackTraceEntry]): string = +proc `$`*(stackTraceEntries: seq[StackTraceEntry]): string {.raises: [].} = result = "" when defined(nimStackTraceOverride): let entries = addDebuggingInfo(stackTraceEntries) @@ -384,6 +387,28 @@ proc read*[T](future: Future[T] | FutureVar[T]): lent T = proc read*(future: Future[void] | FutureVar[void]) = readImpl(future, void) +macro readTrackedImpl(future: FutureEx): untyped = + # XXX refactor readImpl + let t = getTypeInst(future)[1] + let e = getTypeInst(future)[2] + let types = getType(e) + var raisesList = newNimNode(nnkBracket) + for r in types[1..^1]: + raisesList.add(r) + #echo repr raisesList + #echo repr t + let raisesCast = quote do: + cast(raises: `raisesList`) + quote do: + {.`raisesCast`.}: + readImpl(`future`, `t`) + +proc read*[T, E](future: FutureEx[T, E]): lent T = + readTrackedImpl(future) + +proc read*[E](future: FutureEx[void, E]) = + readTrackedImpl(future) + proc readError*[T](future: Future[T]): ref Exception = ## Retrieves the exception stored in `future`. ## diff --git a/lib/pure/asyncmacro.nim b/lib/pure/asyncmacro.nim index 951d98bd39dd..5783bde23071 100644 --- a/lib/pure/asyncmacro.nim +++ b/lib/pure/asyncmacro.nim @@ -9,7 +9,7 @@ ## Implements the `async` and `multisync` macros for `asyncdispatch`. -import std/[macros, strutils, asyncfutures] +import std/[macros, strutils, asyncfutures, effecttraits] type Context = ref object @@ -49,9 +49,10 @@ template createCb(futTyp, strName, identName, futureVarCompletions: untyped) = except: futureVarCompletions if fut.finished: - # Take a look at tasyncexceptions for the bug which this fixes. - # That test explains it better than I can here. - raise + {.cast(raises: []).}: + # Take a look at tasyncexceptions for the bug which this fixes. + # That test explains it better than I can here. + raise else: fut.fail(getCurrentException()) {.pop.} @@ -158,6 +159,52 @@ proc verifyReturnType(typeName: string, node: NimNode = nil) = error("Expected return type of 'Future' got '$1'" % typeName, node) +proc isAsyncPrc0(n: NimNode): bool = + if n.kind == nnkBlockStmt and n[0].strVal == "asynctrack": + return true + if n.kind in RoutineNodes: + return false + for i in 0 .. n.len-1: + if isAsyncPrc0(n[i]): + return true + return false + +proc isAsyncPrc(n: NimNode): bool = + for i in 0 .. n.len-1: + if isAsyncPrc0(n[i]): + return true + return false + +macro withRaises[T](f: Future[T], body: untyped): untyped = + #echo repr f.kind + # XXX support more cases + let prcSym = case f.kind + of nnkSym: + if f.getImpl.kind == nnkIdentDefs and f.getImpl[^1].kind == nnkCall: + #echo repr f.getImpl[^1][0] + f.getImpl[^1][0] + else: + nil + of nnkCall: + if f[0].kind == nnkSym: f[0] else: nil + else: + nil + #echo repr prcSym + #echo repr prcSym.getImpl + if prcSym != nil and isAsyncPrc(prcSym.getImpl): + let raisesList = getRaisesList(prcSym) + var raisesListTyp = newNimNode(nnkBracket) + if raisesList.len > 0: + for r in raisesList[1..^1]: + raisesListTyp.add(r) + let raisesCast = quote do: + cast(raises: `raisesListTyp`) + quote do: + {.`raisesCast`.}: + `body` + else: + body + template await*(f: typed): untyped {.used.} = static: error "await expects Future[T], got " & $typeof(f) @@ -171,7 +218,8 @@ template await*[T](f: Future[T]): auto {.used.} = var internalTmpFuture: FutureBase = f yield internalTmpFuture {.line: instantiationInfo(fullPaths = true).}: - (cast[typeof(f)](internalTmpFuture)).read() + withRaises f: + cast[typeof(f)](internalTmpFuture).read() else: macro errorAsync(futureError: Future[T]) = error( @@ -180,6 +228,16 @@ template await*[T](f: Future[T]): auto {.used.} = futureError) errorAsync(f) +template await*[T, E](f: FutureEx[T, E]): untyped = + template yieldFuture = yield FutureBase() + when compiles(yieldFuture): + var internalTmpFuture: FutureBase = Future[T](f) + yield internalTmpFuture + {.line: instantiationInfo(fullPaths = true).}: + cast[typeof(f)](internalTmpFuture).read() + else: + {.error: "await is only available within {.async.}".} + proc asyncSingleProc(prc: NimNode): NimNode = ## This macro transforms a single procedure into a closure iterator. ## The `async` macro supports a stmtList holding multiple async procedures. @@ -321,8 +379,10 @@ proc asyncSingleProc(prc: NimNode): NimNode = # based on the yglukhov's patch to chronos: https://github.com/status-im/nim-chronos/pull/47 if procBody.kind != nnkEmpty: + let asynctrack = ident"asynctrack" body2.add quote do: - `outerProcBody` + block `asynctrack`: + `outerProcBody` result.body = body2 macro async*(prc: untyped): untyped = @@ -394,3 +454,24 @@ macro multisync*(prc: untyped): untyped = result = newStmtList() result.add(asyncSingleProc(asyncPrc)) result.add(sync) + +macro toFutureEx*(prc: typed): untyped = + template check(cond: untyped): untyped = + if not cond: + error("async proc call expected", prc) + check prc.kind == nnkCall + check prc[0].kind == nnkSym + check isAsyncPrc(prc[0].getImpl) + let procImpl = getTypeImpl(prc[0]) + check procImpl.kind == nnkProcTy + let retTyp = procImpl.params[0] + let baseTyp = retTyp[1] + let raisesList = getRaisesList(prc[0]) + let exTyp = if raisesList.len == 0: + newIdentNode("void") + else: + newNimNode(nnkTupleConstr) + for r in raisesList: + exTyp.add r + result = quote do: + FutureEx[`baseTyp`, `exTyp`](`prc`) diff --git a/nimdoc/testproject/expected/testproject.html b/nimdoc/testproject/expected/testproject.html index 43a72d99db0c..a269412ca3db 100644 --- a/nimdoc/testproject/expected/testproject.html +++ b/nimdoc/testproject/expected/testproject.html @@ -559,8 +559,7 @@

Procs

-
proc asyncFun1(): Future[int] {....stackTrace: false,
-                                raises: [Exception, ValueError],
+  
proc asyncFun1(): Future[int] {....stackTrace: false, raises: [],
                                 tags: [RootEffect], forbids: [].}
@@ -572,7 +571,7 @@

Procs

-
proc asyncFun2(): owned(Future[void]) {....stackTrace: false, raises: [Exception],
+  
proc asyncFun2(): owned(Future[void]) {....stackTrace: false, raises: [],
                                         tags: [RootEffect], forbids: [].}
@@ -584,7 +583,7 @@

Procs

-
proc asyncFun3(): owned(Future[void]) {....stackTrace: false, raises: [Exception],
+  
proc asyncFun3(): owned(Future[void]) {....stackTrace: false, raises: [],
                                         tags: [RootEffect], forbids: [].}
diff --git a/tests/async/tasync_error_tracking.nim b/tests/async/tasync_error_tracking.nim new file mode 100644 index 000000000000..54e5e8cf1867 --- /dev/null +++ b/tests/async/tasync_error_tracking.nim @@ -0,0 +1,145 @@ +import std/asyncdispatch + +type MyError = object of ValueError + +proc err(throw: bool) = + if throw: + raise newException(MyError, "myerr") + +block: + proc bar {.async.} = + err(false) + + proc foo {.async.} = + await bar() + + proc main {.async, raises: [MyError].} = + await foo() + + waitFor main() + +block: + proc foo {.async, raises: [MyError].} = + err(false) + + proc main {.async, raises: [MyError].} = + await foo() + + waitFor main() + +block: + proc foo {.async, raises: [MyError].} = + err(false) + + proc bar(fut: FutureEx[void, (MyError,)]) {.async, raises: [MyError].} = + await fut + + proc main {.async, raises: [MyError].} = + let fooFut = toFutureEx foo() + await bar(fooFut) + + waitFor main() + +block: + proc foo: Future[int] {.async, raises: [].} = + discard + +block: + # we cannot tell if fcb is an async proc + # or a closure that returns a user created newFuture() + # that can raise anything + type FooBar = object + fcb: proc(): Future[void] {.closure, gcsafe.} + + proc bar {.async.} = + discard + + proc foo {.async.} = + var f = FooBar(fcb: bar) + await f.fcb() + + template good = + proc main {.async, raises: [Exception].} = + await foo() + template bad = + proc main {.async, raises: [].} = + await foo() + doAssert compiles(good()) + doAssert not compiles(bad()) + +block: + template good = + proc foo {.async, raises: [MyError].} = + err(false) + template bad = + proc foo {.async, raises: [].} = + err(false) + doAssert compiles(good()) + doAssert not compiles(bad()) + +block: + proc bar {.async.} = + err(false) + + proc foo {.async.} = + await bar() + + template good = + proc main {.async, raises: [MyError].} = + await foo() + template bad = + proc main {.async, raises: [].} = + await foo() + doAssert compiles(good()) + doAssert not compiles(bad()) + +block: + template good = + proc foo(fut: FutureEx[void, (MyError,)]) {.async, raises: [MyError].} = + await fut + template bad = + proc foo(fut: FutureEx[void, (MyError,)]) {.async, raises: [].} = + await fut + doAssert compiles(good()) + doAssert not compiles(bad()) + +block: + template good = + proc foo {.async, raises: [Exception].} = + await newFuture[void]() + template bad = + proc foo {.async, raises: [].} = + await newFuture[void]() + doAssert compiles(good()) + doAssert not compiles(bad()) + +block: + proc fut: Future[void] = + newFuture[void]() + + template good = + proc main {.async, raises: [Exception].} = + await fut() + template bad = + proc main {.async, raises: [].} = + await fut() + doAssert compiles(good()) + doAssert not compiles(bad()) + +block: + proc bar() {.async.} = + err(false) + + # XXX We could check all returns are from async procs + # and if so use the inferred proc raises + proc foo(): Future[void] = + bar() + + template good = + proc main {.async, raises: [Exception].} = + await foo() + template bad = + proc main {.async, raises: [MyError].} = + await foo() + doAssert compiles(good()) + doAssert not compiles(bad())