diff --git a/changelog.md b/changelog.md
index f32d6f19f6..287bce3dad 100644
--- a/changelog.md
+++ b/changelog.md
@@ -116,6 +116,7 @@
- Add `random.gauss`, that uses the ratio of uniforms method of sampling from a Gaussian distribution.
- Add `typetraits.elementType` to get element type of an iterable.
- `typetraits.$`: `$(int,)` is now `"(int,)"`; `$tuple[]` is now `"tuple[]"`
+- add `macros.extractDocCommentsAndRunnables` helper
- `strformat.fmt` and `strformat.&` support `= specifier`. `fmt"{expr=}"` now expands to `fmt"expr={expr}"`.
diff --git a/lib/core/macros.nim b/lib/core/macros.nim
index 5be3a2cce0..8387072803 100644
--- a/lib/core/macros.nim
+++ b/lib/core/macros.nim
@@ -1692,3 +1692,39 @@ when defined(nimMacrosSizealignof):
proc isExported*(n: NimNode): bool {.noSideEffect.} =
## Returns whether the symbol is exported or not.
+
+proc extractDocCommentsAndRunnables*(n: NimNode): NimNode =
+ ## returns a `nnkStmtList` containing the top-level doc comments and
+ ## runnableExamples in `a`, stopping at the first child that is neither.
+ ## Example:
+ ##
+ ## .. code-block:: nim
+ ## import macros
+ ## macro transf(a): untyped =
+ ## result = quote do:
+ ## proc fun2*() = discard
+ ## let header = extractDocCommentsAndRunnables(a.body)
+ ## # correct usage: rest is appended
+ ## result.body = header
+ ## result.body.add quote do: discard # just an example
+ ## # incorrect usage: nesting inside a nnkStmtList:
+ ## # result.body = quote do: (`header`; discard)
+ ##
+ ## proc fun*() {.transf.} =
+ ## ## first comment
+ ## runnableExamples: discard
+ ## runnableExamples: discard
+ ## ## last comment
+ ## discard # first statement after doc comments + runnableExamples
+ ## ## not docgen'd
+
+ result = newStmtList()
+ for ni in n:
+ case ni.kind
+ of nnkCommentStmt:
+ result.add ni
+ of nnkCall:
+ if ni[0].kind == nnkIdent and ni[0].strVal == "runnableExamples":
+ result.add ni
+ else: break
+ else: break
diff --git a/lib/pure/asyncmacro.nim b/lib/pure/asyncmacro.nim
index 621a4b00c5..219ef6c675 100644
--- a/lib/pure/asyncmacro.nim
+++ b/lib/pure/asyncmacro.nim
@@ -180,8 +180,7 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
# Extract the documentation comment from the original procedure declaration.
# Note that we're not removing it from the body in order not to make this
# transformation even more complex.
- if prc.body.len > 1 and prc.body[0].kind == nnkCommentStmt:
- outerProcBody.add(prc.body[0])
+ let body2 = extractDocCommentsAndRunnables(prc.body)
# -> var retFuture = newFuture[T]()
var retFutureSym = genSym(nskVar, "retFuture")
@@ -276,9 +275,10 @@ proc asyncSingleProc(prc: NimNode): NimNode {.compileTime.} =
(cast[type(f)](internalTmpFuture)).read()
if procBody.kind != nnkEmpty:
- result.body = quote:
+ body2.add quote do:
`awaitDefinition`
`outerProcBody`
+ result.body = body2
#echo(treeRepr(result))
#if prcName == "recvLineInto":
diff --git a/nimdoc/testproject/expected/testproject.html b/nimdoc/testproject/expected/testproject.html
index a4f8876fd8..648ecb6294 100644
--- a/nimdoc/testproject/expected/testproject.html
+++ b/nimdoc/testproject/expected/testproject.html
@@ -200,6 +200,12 @@ function main() {
title="low2[T: Ordinal | enum | range](x: T): T">
proc asyncFun1(): Future[int] {...}{.raises: [Exception, ValueError], tags: [RootEffect].}
proc asyncFun2(): owned(Future[void]) {...}{.raises: [Exception], tags: [RootEffect].}
proc asyncFun3(): owned(Future[void]) {...}{.raises: [Exception], tags: [RootEffect].}
Example:
+discardok1
+