summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--changelog.md2
-rw-r--r--lib/pure/asyncfutures.nim79
-rw-r--r--lib/pure/asyncmacro.nim15
-rw-r--r--tests/async/tasync_traceback.nim116
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