diff options
-rw-r--r-- | changelog.md | 2 | ||||
-rw-r--r-- | lib/pure/asyncfutures.nim | 79 | ||||
-rw-r--r-- | lib/pure/asyncmacro.nim | 15 | ||||
-rw-r--r-- | tests/async/tasync_traceback.nim | 116 |
4 files changed, 197 insertions, 15 deletions
diff --git a/changelog.md b/changelog.md index de4b2f251..1c5848ce8 100644 --- a/changelog.md +++ b/changelog.md @@ -111,6 +111,8 @@ This now needs to be written as: - The ``[]`` proc for strings now raises an ``IndexError`` exception when the specified slice is out of bounds. See issue [#6223](https://github.com/nim-lang/Nim/issues/6223) for more details. + You can use ``substr(str, start, finish)`` to get the old behaviour back, + see [this commit](https://github.com/nim-lang/nimbot/commit/98cc031a27ea89947daa7f0bb536bcf86462941f) for an example. - ``strutils.split`` and ``strutils.rsplit`` with an empty string and a separator now returns that empty string. See issue [#4377](https://github.com/nim-lang/Nim/issues/4377). diff --git a/lib/pure/asyncfutures.nim b/lib/pure/asyncfutures.nim index 4bd3227a1..bcc3ab613 100644 --- a/lib/pure/asyncfutures.nim +++ b/lib/pure/asyncfutures.nim @@ -1,4 +1,4 @@ -import os, tables, strutils, times, heapqueue, options, deques +import os, tables, strutils, times, heapqueue, options, deques, cstrutils # TODO: This shouldn't need to be included, but should ideally be exported. type @@ -217,17 +217,78 @@ proc `callback=`*[T](future: Future[T], ## If future has already completed then ``cb`` will be called immediately. future.callback = proc () = cb(future) +proc getHint(entry: StackTraceEntry): string = + ## We try to provide some hints about stack trace entries that the user + ## may not be familiar with, in particular calls inside the stdlib. + result = "" + if entry.procname == "processPendingCallbacks": + if cmpIgnoreStyle(entry.filename, "asyncdispatch.nim") == 0: + return "Executes pending callbacks" + elif entry.procname == "poll": + if cmpIgnoreStyle(entry.filename, "asyncdispatch.nim") == 0: + return "Processes asynchronous completion events" + + if entry.procname.endsWith("_continue"): + if cmpIgnoreStyle(entry.filename, "asyncmacro.nim") == 0: + return "Resumes an async procedure" + +proc `$`*(entries: seq[StackTraceEntry]): string = + result = "" + # Find longest filename & line number combo for alignment purposes. + var longestLeft = 0 + for entry in entries: + if entry.procName.isNil: continue + + let left = $entry.filename & $entry.line + if left.len > longestLeft: + longestLeft = left.len + + var indent = 2 + # Format the entries. + for entry in entries: + if entry.procName.isNil: + if entry.line == -10: + result.add(spaces(indent) & "#[\n") + indent.inc(2) + else: + indent.dec(2) + result.add(spaces(indent)& "]#\n") + continue + + let left = "$#($#)" % [$entry.filename, $entry.line] + result.add((spaces(indent) & "$#$# $#\n") % [ + left, + spaces(longestLeft - left.len + 2), + $entry.procName + ]) + let hint = getHint(entry) + if hint.len > 0: + result.add(spaces(indent+2) & "## " & hint & "\n") + proc injectStacktrace[T](future: Future[T]) = - # TODO: Come up with something better. when not defined(release): - var msg = "" - msg.add("\n " & future.fromProc & "'s lead up to read of failed Future:") + const header = "\nAsync traceback:\n" - if not future.errorStackTrace.isNil and future.errorStackTrace != "": - msg.add("\n" & indent(future.errorStackTrace.strip(), 4)) - else: - msg.add("\n Empty or nil stack trace.") - future.error.msg.add(msg) + var exceptionMsg = future.error.msg + if header in exceptionMsg: + # This is messy: extract the original exception message from the msg + # containing the async traceback. + let start = exceptionMsg.find(header) + exceptionMsg = exceptionMsg[0..<start] + + + var newMsg = exceptionMsg & header + + let entries = getStackTraceEntries(future.error) + newMsg.add($entries) + + newMsg.add("Exception message: " & exceptionMsg & "\n") + newMsg.add("Exception type:") + + # # For debugging purposes + # for entry in getStackTraceEntries(future.error): + # newMsg.add "\n" & $entry + future.error.msg = newMsg proc read*[T](future: Future[T] | FutureVar[T]): T = ## Retrieves the value of ``future``. Future must be finished otherwise diff --git a/lib/pure/asyncmacro.nim b/lib/pure/asyncmacro.nim index a8e378d5c..8c679929d 100644 --- a/lib/pure/asyncmacro.nim +++ b/lib/pure/asyncmacro.nim @@ -25,10 +25,10 @@ proc skipStmtList(node: NimNode): NimNode {.compileTime.} = result = node[0] template createCb(retFutureSym, iteratorNameSym, - name, futureVarCompletions: untyped) = + strName, identName, futureVarCompletions: untyped) = var nameIterVar = iteratorNameSym #{.push stackTrace: off.} - proc cb0 {.closure.} = + proc identName {.closure.} = try: if not nameIterVar.finished: var next = nameIterVar() @@ -36,11 +36,11 @@ template createCb(retFutureSym, iteratorNameSym, if not retFutureSym.finished: let msg = "Async procedure ($1) yielded `nil`, are you await'ing a " & "`nil` Future?" - raise newException(AssertionError, msg % name) + raise newException(AssertionError, msg % strName) else: {.gcsafe.}: {.push hint[ConvFromXtoItselfNotNeeded]: off.} - next.callback = (proc() {.closure, gcsafe.})(cb0) + next.callback = (proc() {.closure, gcsafe.})(identName) {.pop.} except: futureVarCompletions @@ -52,7 +52,7 @@ template createCb(retFutureSym, iteratorNameSym, else: retFutureSym.fail(getCurrentException()) - cb0() + identName() #{.pop.} proc generateExceptionCheck(futSym, tryStmt, rootReceiver, fromNode: NimNode): NimNode {.compileTime.} = @@ -389,9 +389,12 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} = outerProcBody.add(closureIterator) # -> createCb(retFuture) - #var cbName = newIdentNode("cb") + # NOTE: The "_continue" suffix is checked for in asyncfutures.nim to produce + # friendlier stack traces: + var cbName = genSym(nskProc, prcName & "_continue") var procCb = getAst createCb(retFutureSym, iteratorNameSym, newStrLitNode(prcName), + cbName, createFutureVarCompletions(futureVarIdents, nil)) outerProcBody.add procCb diff --git a/tests/async/tasync_traceback.nim b/tests/async/tasync_traceback.nim new file mode 100644 index 000000000..08f7e7317 --- /dev/null +++ b/tests/async/tasync_traceback.nim @@ -0,0 +1,116 @@ +discard """ + exitcode: 0 + disabled: "windows" + output: ''' +b failure +Async traceback: + tasync_traceback.nim(97) tasync_traceback + asyncmacro.nim(395) a + asyncmacro.nim(34) a_continue + ## Resumes an async procedure + tasync_traceback.nim(95) aIter + asyncmacro.nim(395) b + asyncmacro.nim(34) b_continue + ## Resumes an async procedure + tasync_traceback.nim(92) bIter + #[ + tasync_traceback.nim(97) tasync_traceback + asyncmacro.nim(395) a + asyncmacro.nim(43) a_continue + ## Resumes an async procedure + asyncfutures.nim(211) callback= + asyncfutures.nim(190) addCallback + asyncfutures.nim(53) callSoon + asyncmacro.nim(34) a_continue + ## Resumes an async procedure + asyncmacro.nim(0) aIter + asyncfutures.nim(304) read + ]# +Exception message: b failure +Exception type: + +bar failure +Async traceback: + tasync_traceback.nim(113) tasync_traceback + asyncdispatch.nim(1492) waitFor + asyncdispatch.nim(1496) poll + ## Processes asynchronous completion events + asyncdispatch.nim(1262) runOnce + asyncdispatch.nim(183) processPendingCallbacks + ## Executes pending callbacks + asyncmacro.nim(34) bar_continue + ## Resumes an async procedure + tasync_traceback.nim(108) barIter + #[ + tasync_traceback.nim(113) tasync_traceback + asyncdispatch.nim(1492) waitFor + asyncdispatch.nim(1496) poll + ## Processes asynchronous completion events + asyncdispatch.nim(1262) runOnce + asyncdispatch.nim(183) processPendingCallbacks + ## Executes pending callbacks + asyncmacro.nim(34) foo_continue + ## Resumes an async procedure + asyncmacro.nim(0) fooIter + asyncfutures.nim(304) read + ]# +Exception message: bar failure +Exception type:''' +""" +import asyncdispatch + +# Tests to ensure our exception trace backs are friendly. + +# --- Simple test. --- +# +# What does this look like when it's synchronous? +# +# tasync_traceback.nim(23) tasync_traceback +# tasync_traceback.nim(21) a +# tasync_traceback.nim(18) b +# Error: unhandled exception: b failure [OSError] +# +# Good (not quite ideal, but gotta work within constraints) traceback, +# when exception is unhandled: +# +# <traceback for the unhandled exception> +# <very much a bunch of noise> +# <would be ideal to customise this> +# <(the code responsible is in excpt:raiseExceptionAux)> +# Error: unhandled exception: b failure +# =============== +# Async traceback +# =============== +# +# tasync_traceback.nim(23) tasync_traceback +# +# tasync_traceback.nim(21) a +# tasync_traceback.nim(18) b + +proc b(): Future[int] {.async.} = + if true: + raise newException(OSError, "b failure") + +proc a(): Future[int] {.async.} = + return await b() + +let aFut = a() +try: + discard waitFor aFut +except Exception as exc: + echo exc.msg +echo() + +# From #6803 +proc bar(): Future[string] {.async.} = + await sleepAsync(100) + if true: + raise newException(OSError, "bar failure") + +proc foo(): Future[string] {.async.} = return await bar() + +try: + echo waitFor(foo()) +except Exception as exc: + echo exc.msg +echo() \ No newline at end of file |