asyncmacro.nim 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2015 Dominik Picheta
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## Implements the `async` and `multisync` macros for `asyncdispatch`.
  10. import macros, strutils, asyncfutures
  11. type
  12. Context = ref object
  13. inTry: int
  14. hasRet: bool
  15. # TODO: Ref https://github.com/nim-lang/Nim/issues/5617
  16. # TODO: Add more line infos
  17. proc newCallWithLineInfo(fromNode: NimNode; theProc: NimNode, args: varargs[NimNode]): NimNode =
  18. result = newCall(theProc, args)
  19. result.copyLineInfo(fromNode)
  20. template createCb(retFutureSym, iteratorNameSym,
  21. strName, identName, futureVarCompletions: untyped) =
  22. bind finished
  23. var nameIterVar = iteratorNameSym
  24. proc identName {.closure, stackTrace: off.} =
  25. try:
  26. if not nameIterVar.finished:
  27. var next = nameIterVar()
  28. # Continue while the yielded future is already finished.
  29. while (not next.isNil) and next.finished:
  30. next = nameIterVar()
  31. if nameIterVar.finished:
  32. break
  33. if next == nil:
  34. if not retFutureSym.finished:
  35. let msg = "Async procedure ($1) yielded `nil`, are you await'ing a `nil` Future?"
  36. raise newException(AssertionDefect, msg % strName)
  37. else:
  38. {.gcsafe.}:
  39. next.addCallback cast[proc() {.closure, gcsafe.}](identName)
  40. except:
  41. futureVarCompletions
  42. if retFutureSym.finished:
  43. # Take a look at tasyncexceptions for the bug which this fixes.
  44. # That test explains it better than I can here.
  45. raise
  46. else:
  47. retFutureSym.fail(getCurrentException())
  48. identName()
  49. proc createFutureVarCompletions(futureVarIdents: seq[NimNode], fromNode: NimNode): NimNode =
  50. result = newNimNode(nnkStmtList, fromNode)
  51. # Add calls to complete each FutureVar parameter.
  52. for ident in futureVarIdents:
  53. # Only complete them if they have not been completed already by the user.
  54. # In the meantime, this was really useful for debugging :)
  55. #result.add(newCall(newIdentNode("echo"), newStrLitNode(fromNode.lineinfo)))
  56. result.add newIfStmt(
  57. (
  58. newCall(newIdentNode("not"),
  59. newDotExpr(ident, newIdentNode("finished"))),
  60. newCallWithLineInfo(fromNode, newIdentNode("complete"), ident)
  61. )
  62. )
  63. proc processBody(ctx: Context; node, needsCompletionSym, retFutureSym: NimNode, futureVarIdents: seq[NimNode]): NimNode =
  64. result = node
  65. case node.kind
  66. of nnkReturnStmt:
  67. result = newNimNode(nnkStmtList, node)
  68. # As I've painfully found out, the order here really DOES matter.
  69. result.add createFutureVarCompletions(futureVarIdents, node)
  70. ctx.hasRet = true
  71. if node[0].kind == nnkEmpty:
  72. if ctx.inTry == 0:
  73. result.add newCallWithLineInfo(node, newIdentNode("complete"), retFutureSym, newIdentNode("result"))
  74. else:
  75. result.add newAssignment(needsCompletionSym, newLit(true))
  76. else:
  77. let x = processBody(ctx, node[0], needsCompletionSym, retFutureSym, futureVarIdents)
  78. if x.kind == nnkYieldStmt: result.add x
  79. elif ctx.inTry == 0:
  80. result.add newCallWithLineInfo(node, newIdentNode("complete"), retFutureSym, x)
  81. else:
  82. result.add newAssignment(newIdentNode("result"), x)
  83. result.add newAssignment(needsCompletionSym, newLit(true))
  84. result.add newNimNode(nnkReturnStmt, node).add(newNilLit())
  85. return # Don't process the children of this return stmt
  86. of RoutineNodes-{nnkTemplateDef}:
  87. # skip all the nested procedure definitions
  88. return
  89. of nnkTryStmt:
  90. if result[^1].kind == nnkFinally:
  91. inc ctx.inTry
  92. result[0] = processBody(ctx, result[0], needsCompletionSym, retFutureSym, futureVarIdents)
  93. dec ctx.inTry
  94. for i in 1 ..< result.len:
  95. result[i] = processBody(ctx, result[i], needsCompletionSym, retFutureSym, futureVarIdents)
  96. if ctx.inTry == 0 and ctx.hasRet:
  97. let finallyNode = copyNimNode(result[^1])
  98. let stmtNode = newNimNode(nnkStmtList)
  99. for child in result[^1]:
  100. stmtNode.add child
  101. stmtNode.add newIfStmt(
  102. ( needsCompletionSym,
  103. newCallWithLineInfo(node, newIdentNode("complete"), retFutureSym,
  104. newIdentNode("result")
  105. )
  106. )
  107. )
  108. finallyNode.add stmtNode
  109. result[^1] = finallyNode
  110. else:
  111. for i in 0 ..< result.len:
  112. result[i] = processBody(ctx, result[i], needsCompletionSym, retFutureSym, futureVarIdents)
  113. else:
  114. for i in 0 ..< result.len:
  115. result[i] = processBody(ctx, result[i], needsCompletionSym, retFutureSym, futureVarIdents)
  116. # echo result.repr
  117. proc getName(node: NimNode): string =
  118. case node.kind
  119. of nnkPostfix:
  120. return node[1].strVal
  121. of nnkIdent, nnkSym:
  122. return node.strVal
  123. of nnkEmpty:
  124. return "anonymous"
  125. else:
  126. error("Unknown name.", node)
  127. proc getFutureVarIdents(params: NimNode): seq[NimNode] =
  128. result = @[]
  129. for i in 1 ..< len(params):
  130. expectKind(params[i], nnkIdentDefs)
  131. if params[i][1].kind == nnkBracketExpr and
  132. params[i][1][0].eqIdent(FutureVar.astToStr):
  133. ## eqIdent: first char is case sensitive!!!
  134. result.add(params[i][0])
  135. proc isInvalidReturnType(typeName: string): bool =
  136. return typeName notin ["Future"] #, "FutureStream"]
  137. proc verifyReturnType(typeName: string, node: NimNode = nil) =
  138. if typeName.isInvalidReturnType:
  139. error("Expected return type of 'Future' got '$1'" %
  140. typeName, node)
  141. template await*(f: typed): untyped {.used.} =
  142. static:
  143. error "await expects Future[T], got " & $typeof(f)
  144. template await*[T](f: Future[T]): auto {.used.} =
  145. when not defined(nimHasTemplateRedefinitionPragma):
  146. {.pragma: redefine.}
  147. template yieldFuture {.redefine.} = yield FutureBase()
  148. when compiles(yieldFuture):
  149. var internalTmpFuture: FutureBase = f
  150. yield internalTmpFuture
  151. (cast[typeof(f)](internalTmpFuture)).read()
  152. else:
  153. macro errorAsync(futureError: Future[T]) =
  154. error(
  155. "Can only 'await' inside a proc marked as 'async'. Use " &
  156. "'waitFor' when calling an 'async' proc in a non-async scope instead",
  157. futureError)
  158. errorAsync(f)
  159. proc asyncSingleProc(prc: NimNode): NimNode =
  160. ## This macro transforms a single procedure into a closure iterator.
  161. ## The `async` macro supports a stmtList holding multiple async procedures.
  162. if prc.kind == nnkProcTy:
  163. result = prc
  164. if prc[0][0].kind == nnkEmpty:
  165. result[0][0] = quote do: Future[void]
  166. return result
  167. if prc.kind in RoutineNodes and prc.name.kind != nnkEmpty:
  168. # Only non anonymous functions need/can have stack trace disabled
  169. prc.addPragma(nnkExprColonExpr.newTree(ident"stackTrace", ident"off"))
  170. if prc.kind notin {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
  171. error("Cannot transform this node kind into an async proc." &
  172. " proc/method definition or lambda node expected.", prc)
  173. if prc[4].kind != nnkEmpty:
  174. for prag in prc[4]:
  175. if prag.eqIdent("discardable"):
  176. error("Cannot make async proc discardable. Futures have to be " &
  177. "checked with `asyncCheck` instead of discarded", prag)
  178. let prcName = prc.name.getName
  179. var returnType = prc.params[0]
  180. var baseType: NimNode
  181. if returnType.kind in nnkCallKinds and returnType[0].eqIdent("owned") and
  182. returnType.len == 2:
  183. returnType = returnType[1]
  184. # Verify that the return type is a Future[T]
  185. if returnType.kind == nnkBracketExpr:
  186. let fut = repr(returnType[0])
  187. verifyReturnType(fut, returnType[0])
  188. baseType = returnType[1]
  189. elif returnType.kind in nnkCallKinds and returnType[0].eqIdent("[]"):
  190. let fut = repr(returnType[1])
  191. verifyReturnType(fut, returnType[0])
  192. baseType = returnType[2]
  193. elif returnType.kind == nnkEmpty:
  194. baseType = returnType
  195. else:
  196. verifyReturnType(repr(returnType), returnType)
  197. let futureVarIdents = getFutureVarIdents(prc.params)
  198. var outerProcBody = newNimNode(nnkStmtList, prc.body)
  199. # Extract the documentation comment from the original procedure declaration.
  200. # Note that we're not removing it from the body in order not to make this
  201. # transformation even more complex.
  202. let body2 = extractDocCommentsAndRunnables(prc.body)
  203. # -> var retFuture = newFuture[T]()
  204. var retFutureSym = genSym(nskVar, "retFuture")
  205. var subRetType =
  206. if returnType.kind == nnkEmpty: newIdentNode("void")
  207. else: baseType
  208. outerProcBody.add(
  209. newVarStmt(retFutureSym,
  210. newCall(
  211. newNimNode(nnkBracketExpr, prc.body).add(
  212. newIdentNode("newFuture"),
  213. subRetType),
  214. newLit(prcName)))) # Get type from return type of this proc
  215. # -> iterator nameIter(): FutureBase {.closure.} =
  216. # -> {.push warning[resultshadowed]: off.}
  217. # -> var result: T
  218. # -> {.pop.}
  219. # -> <proc_body>
  220. # -> complete(retFuture, result)
  221. var iteratorNameSym = genSym(nskIterator, $prcName & " (Async)")
  222. var needsCompletionSym = genSym(nskVar, "needsCompletion")
  223. var ctx = Context()
  224. var procBody = processBody(ctx, prc.body, needsCompletionSym, retFutureSym, futureVarIdents)
  225. # don't do anything with forward bodies (empty)
  226. if procBody.kind != nnkEmpty:
  227. # fix #13899, defer should not escape its original scope
  228. let blockStmt = newStmtList(newTree(nnkBlockStmt, newEmptyNode(), procBody))
  229. procBody = newStmtList()
  230. let resultIdent = ident"result"
  231. procBody.add quote do:
  232. # Check whether there is an implicit return
  233. when typeof(`blockStmt`) is void:
  234. `blockStmt`
  235. else:
  236. `resultIdent` = `blockStmt`
  237. procBody.add(createFutureVarCompletions(futureVarIdents, nil))
  238. procBody.insert(0): quote do:
  239. {.push warning[resultshadowed]: off.}
  240. when `subRetType` isnot void:
  241. var `resultIdent`: `subRetType`
  242. else:
  243. var `resultIdent`: Future[void]
  244. {.pop.}
  245. var `needsCompletionSym` = false
  246. procBody.add quote do:
  247. complete(`retFutureSym`, `resultIdent`)
  248. var closureIterator = newProc(iteratorNameSym, [quote do: owned(FutureBase)],
  249. procBody, nnkIteratorDef)
  250. closureIterator.pragma = newNimNode(nnkPragma, lineInfoFrom = prc.body)
  251. closureIterator.addPragma(newIdentNode("closure"))
  252. # If proc has an explicit gcsafe pragma, we add it to iterator as well.
  253. if prc.pragma.findChild(it.kind in {nnkSym, nnkIdent} and $it == "gcsafe") != nil:
  254. closureIterator.addPragma(newIdentNode("gcsafe"))
  255. outerProcBody.add(closureIterator)
  256. # -> createCb(retFuture)
  257. # NOTE: The NimAsyncContinueSuffix is checked for in asyncfutures.nim to produce
  258. # friendlier stack traces:
  259. var cbName = genSym(nskProc, prcName & NimAsyncContinueSuffix)
  260. var procCb = getAst createCb(retFutureSym, iteratorNameSym,
  261. newStrLitNode(prcName),
  262. cbName,
  263. createFutureVarCompletions(futureVarIdents, nil)
  264. )
  265. outerProcBody.add procCb
  266. # -> return retFuture
  267. outerProcBody.add newNimNode(nnkReturnStmt, prc.body[^1]).add(retFutureSym)
  268. result = prc
  269. # Add discardable pragma.
  270. if returnType.kind == nnkEmpty:
  271. # xxx consider removing `owned`? it's inconsistent with non-void case
  272. result.params[0] = quote do: owned(Future[void])
  273. # based on the yglukhov's patch to chronos: https://github.com/status-im/nim-chronos/pull/47
  274. if procBody.kind != nnkEmpty:
  275. body2.add quote do:
  276. `outerProcBody`
  277. result.body = body2
  278. macro async*(prc: untyped): untyped =
  279. ## Macro which processes async procedures into the appropriate
  280. ## iterators and yield statements.
  281. if prc.kind == nnkStmtList:
  282. result = newStmtList()
  283. for oneProc in prc:
  284. result.add asyncSingleProc(oneProc)
  285. else:
  286. result = asyncSingleProc(prc)
  287. when defined(nimDumpAsync):
  288. echo repr result
  289. proc splitParamType(paramType: NimNode, async: bool): NimNode =
  290. result = paramType
  291. if paramType.kind == nnkInfix and paramType[0].strVal in ["|", "or"]:
  292. let firstAsync = "async" in paramType[1].toStrLit().strVal.normalize
  293. let secondAsync = "async" in paramType[2].toStrLit().strVal.normalize
  294. if firstAsync:
  295. result = paramType[if async: 1 else: 2]
  296. elif secondAsync:
  297. result = paramType[if async: 2 else: 1]
  298. proc stripReturnType(returnType: NimNode): NimNode =
  299. # Strip out the 'Future' from 'Future[T]'.
  300. result = returnType
  301. if returnType.kind == nnkBracketExpr:
  302. let fut = repr(returnType[0])
  303. verifyReturnType(fut, returnType)
  304. result = returnType[1]
  305. proc splitProc(prc: NimNode): (NimNode, NimNode) =
  306. ## Takes a procedure definition which takes a generic union of arguments,
  307. ## for example: proc (socket: Socket | AsyncSocket).
  308. ## It transforms them so that `proc (socket: Socket)` and
  309. ## `proc (socket: AsyncSocket)` are returned.
  310. result[0] = prc.copyNimTree()
  311. # Retrieve the `T` inside `Future[T]`.
  312. let returnType = stripReturnType(result[0][3][0])
  313. result[0][3][0] = splitParamType(returnType, async = false)
  314. for i in 1 ..< result[0][3].len:
  315. # Sync proc (0) -> FormalParams (3) -> IdentDefs, the parameter (i) ->
  316. # parameter type (1).
  317. result[0][3][i][1] = splitParamType(result[0][3][i][1], async=false)
  318. var multisyncAwait = quote:
  319. template await(value: typed): untyped =
  320. value
  321. result[0][^1] = nnkStmtList.newTree(multisyncAwait, result[0][^1])
  322. result[1] = prc.copyNimTree()
  323. if result[1][3][0].kind == nnkBracketExpr:
  324. result[1][3][0][1] = splitParamType(result[1][3][0][1], async = true)
  325. for i in 1 ..< result[1][3].len:
  326. # Async proc (1) -> FormalParams (3) -> IdentDefs, the parameter (i) ->
  327. # parameter type (1).
  328. result[1][3][i][1] = splitParamType(result[1][3][i][1], async = true)
  329. macro multisync*(prc: untyped): untyped =
  330. ## Macro which processes async procedures into both asynchronous and
  331. ## synchronous procedures.
  332. ##
  333. ## The generated async procedures use the `async` macro, whereas the
  334. ## generated synchronous procedures simply strip off the `await` calls.
  335. let (sync, asyncPrc) = splitProc(prc)
  336. result = newStmtList()
  337. result.add(asyncSingleProc(asyncPrc))
  338. result.add(sync)