diff --git a/lib/js/asyncjs.nim b/lib/js/asyncjs.nim index ac61c1a34c1..370551ddead 100644 --- a/lib/js/asyncjs.nim +++ b/lib/js/asyncjs.nim @@ -8,26 +8,15 @@ ## This module implements types and macros for writing asynchronous code ## for the JS backend. It provides tools for interaction with JavaScript async API-s -## and libraries, writing async procedures in Nim and converting callback-based code -## to promises. +## and libraries, writing async procedures in |NimSkull| and converting +## callback-based code to promises. ## -## A Nim procedure is asynchronous when it includes the `{.async.}` pragma. It -## should always have a `Future[T]` return type or not have a return type at all. -## A `Future[void]` return type is assumed by default. +## A |NimSkull| procedure is asynchronous when it includes the `{.async.}` +## pragma. If the return type is not `Future[T]`, it is opaquely turned +## into one first. A `Future[void]` return type is assumed by default. ## ## This is roughly equivalent to the `async` keyword in JavaScript code. ## -## .. code-block:: nim -## proc loadGame(name: string): Future[Game] {.async.} = -## # code -## -## should be equivalent to -## -## .. code-block:: javascript -## async function loadGame(name) { -## // code -## } -## ## A call to an asynchronous procedure usually needs `await` to wait for ## the completion of the `Future`. ## @@ -52,8 +41,10 @@ ## JavaScript compatibility ## ======================== ## -## Nim currently generates `async/await` JavaScript code which is supported in modern -## EcmaScript and most modern versions of browsers, Node.js and Electron. +## |NimSkull| generates JavaScript code that uses the ``Promise`` and ``Error`` +## APIs, both which are supported by most modern versions of browsers, Node.js +## and Electron. +## ## If you need to use this module with older versions of JavaScript, you can ## use a tool that backports the resulting JavaScript code, as babel. @@ -66,96 +57,185 @@ import std/jsffi import std/macros type - Future*[T] = ref object - future*: T - ## Wraps the return type of an asynchronous procedure. - PromiseJs* {.importjs: "Promise".} = ref object - ## A JavaScript Promise. - -proc reraise(e: ref CatchableError) {.asmNoStackFrame, noreturn.} = - {.emit: ["throw new Error(", cstring(e.msg), ");"].} - -proc replaceReturn(node: var NimNode) = - var z = 0 - for s in node: - var son = node[z] - let jsResolve = ident("jsResolve") - if son.kind == nnkReturnStmt: - let value = if son[0].kind != nnkEmpty: nnkCall.newTree(jsResolve, son[0]) - else: nnkCall.newTree(jsResolve) - node[z] = nnkReturnStmt.newTree(value) - elif son.kind == nnkAsgn and son[0].kind == nnkIdent and $son[0] == "result": - node[z] = nnkAsgn.newTree(son[0], nnkCall.newTree(jsResolve, son[1])) - else: - replaceReturn(son) - inc z + ## A JavaScript Promise. -proc isFutureVoid(node: NimNode): bool = - result = node.kind == nnkBracketExpr and - node[0].kind == nnkIdent and $node[0] == "Future" and - node[1].kind == nnkIdent and $node[1] == "void" + Future*[T] = distinct PromiseJs + ## Wraps the return type of an asynchronous procedure. -proc generateJsasync(arg: NimNode): NimNode = - if arg.kind notin {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}: + Error* {.importjs: "Error".} = ref object of Exception + ## https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + message*: cstring + +{.push raises: [].} # the imported procedures don't raise + +proc reject(e: cstring): PromiseJs {.importjs: "Promise.reject(new Error(#))".} +proc reject(e: Error): PromiseJs {.importjs: "Promise.reject(#)".} + +proc resolve(): PromiseJs {.importjs: "(undefined)".} +proc resolve[T](x: T): PromiseJs {.importjs: "Promise.resolve(#)".} + +proc jsCatch(p: PromiseJs, x: proc): PromiseJs {.importjs: "catch".} +proc jsThen(p: PromiseJs, x: proc): PromiseJs {.importjs: "then".} + +{.pop.} + +proc replaceReturn(n: NimNode, with: NimNode): NimNode = + ## In-place replaces all usages of the 'result' identifier in `n` with + ## `with`. + case n.kind + of nnkIdent: + if n.eqIdent("result"): + with + else: + n + of nnkReturnStmt: + let callee = bindSym("resolve") + if with.isNil: + nnkReturnStmt.newTree(newCall(callee)) + elif n[0].kind == nnkEmpty: + nnkReturnStmt.newTree(newCall(callee, with)) + else: + nnkStmtList.newTree( + newAssignment(with, replaceReturn(n[0], with)), + nnkReturnStmt.newTree(newCall(callee, with)) + ) + of RoutineNodes: + n # don't touch nested routines + else: + for i in 0.. 0: - var awaitFunction = quote: - proc await[T](f: Future[T]): T {.importjs: "(await #)", used.} - result.body.add(awaitFunction) - - var resolve: NimNode - if isVoid: - resolve = quote: - proc `jsResolve`: Future[void] {.importjs: "(undefined)", used.} - else: - resolve = quote: - proc jsResolve[T](a: T): Future[T] {.importjs: "#", used.} - proc jsResolve[T](a: Future[T]): Future[T] {.importjs: "#", used.} - result.body.add(resolve) + result.params[0] = fut.getTypeInst()[1] + + if arg.body.len == 0: + return # forward declaration; don't produce a body + + let + real = real.getTypeInst()[1] # unwrap the typedesc + isVoid = real.typeKind == ntyVoid + self = genSym(nskVar, "self") + err = genSym(nskParam, "err") + + proc newDef(name, typ: NimNode): NimNode = + nnkIdentDefs.newTree(name, typ, newEmptyNode()) + + # `runner` is the routine that does the actual work. The original + # procedure is turned into a thunk for invoking the iterator + let runner = newProc(name = arg.name, + params = [bindSym"PromiseJs", + newDef(err, nnkRefTy.newTree( + bindSym"Exception"))], + procType = nnkIteratorDef, + pragmas = nnkPragma.newTree(ident"closure")) + runner.body = nnkStmtList.newTree() + + # inject the ``await`` routines before the body: + let preamble = quote do: + proc genericCatch(e: ref Exception): PromiseJs = + # define outside of the template to reduce executable size + `self`(e) + + template await[T](f: Future[T]): T {.used.} = + var res: T + yield PromiseJs(f).jsCatch(genericCatch).jsThen( + proc(x: sink T): PromiseJs = + res = move x + # resume the coroutine with no error: + return `self`(nil) + ) + if `err` != nil: # handle the error + raise `err` + res + + proc voidCont(): PromiseJs {.used.} = `self`(nil) + + template await(f: Future[void]) {.used.} = + yield PromiseJs(f).jsCatch(genericCatch).jsThen(voidCont) + if `err` != nil: + raise `err` + + runner.body.add(preamble) + + # setup and emit the result variable definition: + var resultVar: NimNode = nil + if not isVoid: + resultVar = genSym(nskVar, "res") + runner.body.add nnkVarSection.newTree( + nnkIdentDefs.newTree(resultVar, real, newEmptyNode())) + + # now comes the patched body: + for child in replaceReturn(arg.body, resultVar).items: + runner.body.add(child) + + # wrap the body in a try/except that turns uncaught exceptions into rejected + # promises: + let body = runner.body + runner.body = quote: + try: + `body` + except CatchableError as e: + return reject(cstring(e.msg)) + except Error as e: + return reject(e) + + runner.body = nnkStmtList.newTree(runner.body) + + # emit the final return statement: + if isVoid: + runner.body.add nnkReturnStmt.newTree( + newCall(bindSym"resolve")) else: - result.body = newEmptyNode() - for child in code: - result.body.add(child) - - if len(code) > 0 and isVoid: - var voidFix = quote: - return `jsResolve`() - result.body.add(voidFix) - - if len(code) > 0: - # turn |NimSkull| outgoing exceptions into JavaScript errors - let body = result.body - result.body = quote: - try: - `body` - except CatchableError as e: - # use .noreturn call to make sure `body` being an expression works - reraise(e) - - let asyncPragma = quote: - {.codegenDecl: "async function $2($3)".} - - result.addPragma(asyncPragma[0]) + runner.body.add nnkReturnStmt.newTree( + newCall(bindSym"resolve", resultVar)) + + let name = runner.name + # emit the start-up thunk: + result.body = quote do: + var `self`: iterator(e: ref Exception): `PromiseJs` + `runner` + `self` = `name` + return `fut`(`self`(nil)) + +template maybeFuture(T): untyped = + # avoids `Future[Future[T]]` + when T is Future: T + else: Future[T] + +template unwrap[T](_: typedesc[Future[T]]): typedesc = T +template unwrap(T: typedesc): typedesc = T + +proc generateJsasync(arg: NimNode): NimNode = + let res = + if arg.params[0].kind == nnkEmpty: + ident"void" + else: + arg.params[0] + + result = newCall(bindSym"asyncAux", + newCall(bindSym"maybeFuture", res), + newCall(bindSym"unwrap", res), + arg) # the original def macro async*(arg: untyped): untyped = - ## Macro which converts normal procedures into - ## javascript-compatible async procedures. + ## Macro that turns normal procedures into awaitable procedures. Within + ## the body, the `await` procedure is available, for awaiting + ## `Future <#Future>` instances. if arg.kind == nnkStmtList: result = newStmtList() for oneProc in arg: @@ -171,11 +251,6 @@ proc newPromise*(handler: proc(resolve: proc())): Future[void] {.importjs: "(new ## A helper for wrapping callback-based functions ## into promises and async procedures. -template maybeFuture(T): untyped = - # avoids `Future[Future[T]]` - when T is Future: T - else: Future[T] - when defined(nimExperimentalAsyncjsThen): import std/private/since since (1, 5, 1): @@ -190,11 +265,6 @@ when defined(nimExperimentalAsyncjsThen): and https://stackoverflow.com/questions/61377358/javascript-wait-for-async-call-to-finish-before-returning-from-function-witho ]# - type Error* {.importjs: "Error".} = ref object of JsRoot - ## https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error - message*: cstring - name*: cstring - type OnReject* = proc(reason: Error) proc then*[T](future: Future[T], onSuccess: proc, onReject: OnReject = nil): auto = diff --git a/tests/js/tasyncjs.nim b/tests/js/tasyncjs.nim index e5be781c5a9..d47639b0ca5 100644 --- a/tests/js/tasyncjs.nim +++ b/tests/js/tasyncjs.nim @@ -111,6 +111,16 @@ proc main() {.async.} = doAssert reason.name == "Error" doAssert "foobar: 7" == $reason.message + block catch_awaited_exception: + # make sure `await` re-raises the exception the promise was rejected with + var raised = false + try: + discard await(fn(7)) + except: + raised = true + + doAssert raised + echo "done" # justified here to make sure we're running this, since it's inside `async` discard main()