From ef701868d1f8ca52b9c57b3dc39b4a1123df9830 Mon Sep 17 00:00:00 2001 From: Timothee Cour Date: Fri, 29 Jan 2021 20:56:47 -0800 Subject: [PATCH 1/8] asyncjs: add then --- lib/js/asyncjs.nim | 22 ++++++++++++++ tests/js/tasync.nim | 70 +++++++++++++++++++++++++++++---------------- 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/lib/js/asyncjs.nim b/lib/js/asyncjs.nim index 76b948e6a29fd..fec51f37637c8 100644 --- a/lib/js/asyncjs.nim +++ b/lib/js/asyncjs.nim @@ -154,3 +154,25 @@ proc newPromise*[T](handler: proc(resolve: proc(response: T))): Future[T] {.impo proc newPromise*(handler: proc(resolve: proc())): Future[void] {.importcpp: "(new Promise(#))".} ## A helper for wrapping callback-based functions ## into promises and async procedures. + +type OnReject* = proc(reason: JsObject) + +proc then*[T, T2](future: Future[T], onSuccess: proc(value: T): T2, onReject: OnReject = nil): Future[T2] = + ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + asm "`result` = `future`.then(`onSuccess`, `onReject`)" + +proc then*[T](future: Future[T], onSuccess: proc(value: T), onReject: OnReject = nil): Future[void] = + ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + asm "`result` = `future`.then(`onSuccess`, `onReject`)" + +proc then*(future: Future[void], onSuccess: proc(), onReject: OnReject = nil): Future[void] = + ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + asm "`result` = `future`.then(`onSuccess`, `onReject`)" + +proc then*[T2](future: Future[void], onSuccess: proc(): T2, onReject: OnReject = nil): Future[T2] = + ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + asm "`result` = `future`.then(`onSuccess`, `onReject`)" + +proc catch*[T](a: Future[T], onReject: OnReject): Future[void] = + ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch + asm "`result` = `a`.catch(`onReject`)" diff --git a/tests/js/tasync.nim b/tests/js/tasync.nim index 31823765195fb..d006119fcab55 100644 --- a/tests/js/tasync.nim +++ b/tests/js/tasync.nim @@ -7,27 +7,49 @@ e import asyncjs -# demonstrate forward definition -# for js -proc y(e: int): Future[string] {.async.} - -proc e: int {.discardable.} = - echo "e" - return 2 - -proc x(e: int): Future[void] {.async.} = - var s = await y(e) - if e > 2: - return - echo s - e() - -proc y(e: int): Future[string] {.async.} = - if e > 0: - return await y(0) - else: - return "x" - - -discard x(2) - +block: + # demonstrate forward definition for js + proc y(e: int): Future[string] {.async.} + + proc e: int {.discardable.} = + echo "e" + return 2 + + proc x(e: int): Future[void] {.async.} = + var s = await y(e) + if e > 2: + return + echo s + e() + + proc y(e: int): Future[string] {.async.} = + if e > 0: + return await y(0) + else: + return "x" + + + discard x(2) + +import sugar +block: + proc fn(n: int): Future[int] {.async.} = + if n > 0: + var ret = 1 + await fn(n-1) + echo ret + return ret + else: + return 10 + discard fn(4) + # discard fn(4).then(a=>a*3) + # discard fn(4).then((a: int) => (echo "gook1")) + # discard fn(4).then((a: int) => (echo "gook1")).then((a: int) => (echo "gook2")) + # discard fn(4).then((a: int) => (echo "gook1")).then(() => (echo "gook2")) + + # discard fn(4).then((a: int) => (echo "gook1"; a*a)).then((a: int) => (echo a)) + # discard fn(4).then((a: int) => (echo "gook1"; float(a*a))).then((a: float) => (echo a)) + # discard fn(4).then((a: int) => a*10).then((a: int) => (echo a)) + # discard fn(4).then(a => a*10).then((a: int) => (echo a)) + + var witness: seq[string] + discard fn(4).then((a: int) => (witness.add $a; a.float*2)).then((a: float) => (witness.add $a)).then(()=>(echo witness)).then(()=>1) From 4d52c87b2d7ad287a083941a58724787a0de32db Mon Sep 17 00:00:00 2001 From: Timothee Cour Date: Fri, 29 Jan 2021 20:57:07 -0800 Subject: [PATCH 2/8] asyncjs: add then --- tests/js/tasync.nim | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/tests/js/tasync.nim b/tests/js/tasync.nim index d006119fcab55..196a61e1ce30c 100644 --- a/tests/js/tasync.nim +++ b/tests/js/tasync.nim @@ -41,15 +41,6 @@ block: else: return 10 discard fn(4) - # discard fn(4).then(a=>a*3) - # discard fn(4).then((a: int) => (echo "gook1")) - # discard fn(4).then((a: int) => (echo "gook1")).then((a: int) => (echo "gook2")) - # discard fn(4).then((a: int) => (echo "gook1")).then(() => (echo "gook2")) - - # discard fn(4).then((a: int) => (echo "gook1"; a*a)).then((a: int) => (echo a)) - # discard fn(4).then((a: int) => (echo "gook1"; float(a*a))).then((a: float) => (echo a)) - # discard fn(4).then((a: int) => a*10).then((a: int) => (echo a)) - # discard fn(4).then(a => a*10).then((a: int) => (echo a)) - var witness: seq[string] - discard fn(4).then((a: int) => (witness.add $a; a.float*2)).then((a: float) => (witness.add $a)).then(()=>(echo witness)).then(()=>1) + discard fn(4). + then((a: int) => (witness.add $a; a.float*2)).then((a: float) => (witness.add $a)).then(()=>(echo witness)).then(()=>1) From 84b00d2ef190152f9541b8512a289b382354a158 Mon Sep 17 00:00:00 2001 From: Timothee Cour Date: Fri, 29 Jan 2021 20:59:01 -0800 Subject: [PATCH 3/8] _ --- tests/js/tasync.nim | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/js/tasync.nim b/tests/js/tasync.nim index 196a61e1ce30c..8de45162715aa 100644 --- a/tests/js/tasync.nim +++ b/tests/js/tasync.nim @@ -32,6 +32,7 @@ block: discard x(2) import sugar + block: proc fn(n: int): Future[int] {.async.} = if n > 0: @@ -42,5 +43,7 @@ block: return 10 discard fn(4) var witness: seq[string] - discard fn(4). - then((a: int) => (witness.add $a; a.float*2)).then((a: float) => (witness.add $a)).then(()=>(echo witness)).then(()=>1) + discard fn(3) + .then((a: int) => (witness.add $a; a.float*2)) + .then((a: float) => (witness.add $a)) + .then(()=>(echo witness)).then(()=>1) From 775b8d4a5295004a7eda04d7df29aeaede592cf1 Mon Sep 17 00:00:00 2001 From: Timothee Cour Date: Wed, 17 Feb 2021 20:13:00 -0800 Subject: [PATCH 4/8] improve tests, changelog, API --- changelog.md | 2 + lib/js/asyncjs.nim | 89 ++++++++++++++++++++++++++++---------- tests/config.nims | 3 ++ tests/js/tasync.nim | 62 ++++++++++++++++++-------- tests/js/tasyncjs_fail.nim | 19 ++++++++ 5 files changed, 134 insertions(+), 41 deletions(-) create mode 100644 tests/js/tasyncjs_fail.nim diff --git a/changelog.md b/changelog.md index 059f8d9863b59..6a38fd0207187 100644 --- a/changelog.md +++ b/changelog.md @@ -203,6 +203,8 @@ provided by the operating system. - Added `-d:nimStrictMode` in CI in several places to ensure code doesn't have certain hints/warnings +- Added `then`, `catch` to `asyncjs`. + ## Tool changes - The rst parser now supports markdown table syntax. diff --git a/lib/js/asyncjs.nim b/lib/js/asyncjs.nim index fec51f37637c8..4e08f3508a21b 100644 --- a/lib/js/asyncjs.nim +++ b/lib/js/asyncjs.nim @@ -57,12 +57,13 @@ ## 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. -import std/jsffi -import std/macros - when not defined(js) and not defined(nimsuggest): {.fatal: "Module asyncjs is designed to be used with the JavaScript backend.".} +import std/jsffi +import std/macros +import std/private/since + type Future*[T] = ref object future*: T @@ -155,24 +156,64 @@ proc newPromise*(handler: proc(resolve: proc())): Future[void] {.importcpp: "(ne ## A helper for wrapping callback-based functions ## into promises and async procedures. -type OnReject* = proc(reason: JsObject) - -proc then*[T, T2](future: Future[T], onSuccess: proc(value: T): T2, onReject: OnReject = nil): Future[T2] = - ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then - asm "`result` = `future`.then(`onSuccess`, `onReject`)" - -proc then*[T](future: Future[T], onSuccess: proc(value: T), onReject: OnReject = nil): Future[void] = - ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then - asm "`result` = `future`.then(`onSuccess`, `onReject`)" - -proc then*(future: Future[void], onSuccess: proc(), onReject: OnReject = nil): Future[void] = - ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then - asm "`result` = `future`.then(`onSuccess`, `onReject`)" - -proc then*[T2](future: Future[void], onSuccess: proc(): T2, onReject: OnReject = nil): Future[T2] = - ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then - asm "`result` = `future`.then(`onSuccess`, `onReject`)" - -proc catch*[T](a: Future[T], onReject: OnReject): Future[void] = - ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch - asm "`result` = `a`.catch(`onReject`)" +when defined(nimExperimentalAsyncjsThen): + since (1, 5, 1): + #[ + TODO: + * map `Promise.all()` + * proc toString*(a: Error): cstring {.importjs: "#.toString()".} + + Note: + We probably can't have a `waitFor` in js in browser (single threaded), but maybe it would be possible + in in nodejs, see https://nodejs.org/api/child_process.html#child_process_child_process_execsync_command_options + 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 RootObj + ## 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, T2](future: Future[T], onSuccess: proc(value: T): T2, onReject: OnReject = nil): Future[T2] = + ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + asm "`result` = `future`.then(`onSuccess`, `onReject`)" + + proc then*[T](future: Future[T], onSuccess: proc(value: T), onReject: OnReject = nil): Future[void] = + ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + asm "`result` = `future`.then(`onSuccess`, `onReject`)" + + proc then*(future: Future[void], onSuccess: proc(), onReject: OnReject = nil): Future[void] = + ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + asm "`result` = `future`.then(`onSuccess`, `onReject`)" + + proc then*[T2](future: Future[void], onSuccess: proc(): T2, onReject: OnReject = nil): Future[T2] = + ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + asm "`result` = `future`.then(`onSuccess`, `onReject`)" + + proc catch*[T](future: Future[T], onReject: OnReject): Future[void] = + ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch + runnableExamples: + from std/sugar import `=>` + from std/strutils import contains + proc fn(n: int): Future[int] {.async.} = + if n >= 7: raise newException(ValueError, "foobar: " & $n) + else: result = n * 2 + proc main() {.async.} = + let x1 = await fn(3) + assert x1 == 3*2 + let x2 = await fn(4) + .then((a: int) => a.float) + .then((a: float) => $a) + assert x2 == "8.0" + + var reason: Error + await fn(6).catch((r: Error) => (reason = r)) + assert reason == nil + await fn(7).catch((r: Error) => (reason = r)) + assert reason != nil + assert "foobar: 7" in $reason.message + discard main() + + asm "`result` = `future`.catch(`onReject`)" diff --git a/tests/config.nims b/tests/config.nims index ac90d37e86c50..47a303e852a63 100644 --- a/tests/config.nims +++ b/tests/config.nims @@ -23,3 +23,6 @@ hint("Processing", off) # switch("define", "nimTestsEnableFlaky") # switch("hint", "ConvFromXtoItselfNotNeeded") + +# experimental API's are enabled in testament, refs https://github.com/timotheecour/Nim/issues/575 +switch("define", "nimExperimentalAsyncjsThen") diff --git a/tests/js/tasync.nim b/tests/js/tasync.nim index 8de45162715aa..e676ba14b2c73 100644 --- a/tests/js/tasync.nim +++ b/tests/js/tasync.nim @@ -2,10 +2,15 @@ discard """ output: ''' x e +done ''' """ -import asyncjs +#[ +xxx move this to tests/stdlib/tasyncjs.nim +]# + +import std/asyncjs block: # demonstrate forward definition for js @@ -28,22 +33,45 @@ block: else: return "x" - discard x(2) -import sugar +import std/sugar +from std/strutils import contains -block: - proc fn(n: int): Future[int] {.async.} = - if n > 0: - var ret = 1 + await fn(n-1) - echo ret - return ret - else: - return 10 - discard fn(4) - var witness: seq[string] - discard fn(3) - .then((a: int) => (witness.add $a; a.float*2)) - .then((a: float) => (witness.add $a)) - .then(()=>(echo witness)).then(()=>1) +var witness: seq[string] + +proc fn(n: int): Future[int] {.async.} = + if n >= 7: + raise newException(ValueError, "foobar: " & $n) + if n > 0: + var ret = 1 + await fn(n-1) + witness.add $(n, ret) + return ret + else: + return 10 + +proc main() {.async.} = + block: # then + let x = await fn(4) + .then((a: int) => a.float) + .then((a: float) => $a) + doAssert x == "14.0" + doAssert witness == @["(1, 11)", "(2, 12)", "(3, 13)", "(4, 14)"] + + doAssert (await fn(2)) == 12 + + let x2 = await fn(4).then((a: int) => (discard)).then(() => 13) + doAssert x2 == 13 + + block: # catch + var reason: Error + await fn(6).then((a: int) => (witness.add $a)).catch((r: Error) => (reason = r)) + doAssert reason == nil + + await fn(7).then((a: int) => (discard)).catch((r: Error) => (reason = r)) + doAssert reason != nil + doAssert reason.name == "Error" + doAssert "foobar: 7" in $reason.message + echo "done" # justified here to make sure we're running this, since it's inside `async` + +discard main() diff --git a/tests/js/tasyncjs_fail.nim b/tests/js/tasyncjs_fail.nim new file mode 100644 index 0000000000000..d41de423d85af --- /dev/null +++ b/tests/js/tasyncjs_fail.nim @@ -0,0 +1,19 @@ +discard """ + outputsub: "Error: unhandled exception: foobar: 13" +""" + +import std/asyncjs +from std/sugar import `=>` + +proc fn(n: int): Future[int] {.async.} = + if n >= 7: raise newException(ValueError, "foobar: " & $n) + else: result = n + +proc main() {.async.} = + let x1 = await fn(6) + doAssert x1 == 6 + await fn(7).catch((a: Error) => (discard)) + let x3 = await fn(13) + doAssert false # shouldn't go here, should fail before + +discard main() From 4cb9c50f5ab984b762223819e4a6dafe3f05fa23 Mon Sep 17 00:00:00 2001 From: Timothee Cour Date: Wed, 17 Feb 2021 22:03:00 -0800 Subject: [PATCH 5/8] fix test --- compiler/nim.nim | 11 +++++++++-- tests/js/tasyncjs_fail.nim | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/compiler/nim.nim b/compiler/nim.nim index 46654e352f352..80a48a3cd04f8 100644 --- a/compiler/nim.nim +++ b/compiler/nim.nim @@ -95,9 +95,16 @@ proc handleCmdLine(cache: IdentCache; conf: ConfigRef) = var cmdPrefix = "" case conf.backend of backendC, backendCpp, backendObjc: discard - of backendJs: cmdPrefix = findNodeJs() & " " + of backendJs: + cmdPrefix = findNodeJs() + cmdPrefix.add " --unhandled-rejections=strict" + #[ + D20210217T215950:here + this flag is needed for node < v15.0.0, otherwise `tasyncjs_fail` would fail, + refs https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode + ]# else: doAssert false, $conf.backend - execExternalProgram(conf, cmdPrefix & output.quoteShell & ' ' & conf.arguments) + execExternalProgram(conf, cmdPrefix & ' ' & output.quoteShell & ' ' & conf.arguments) of cmdDocLike, cmdRst2html, cmdRst2tex: # bugfix(cmdRst2tex was missing) if conf.arguments.len > 0: # reserved for future use diff --git a/tests/js/tasyncjs_fail.nim b/tests/js/tasyncjs_fail.nim index d41de423d85af..b1e5a7bc33fa9 100644 --- a/tests/js/tasyncjs_fail.nim +++ b/tests/js/tasyncjs_fail.nim @@ -1,7 +1,10 @@ discard """ + exitCode: 1 outputsub: "Error: unhandled exception: foobar: 13" """ +# note: this needs `--unhandled-rejections=strict`, see D20210217T215950 + import std/asyncjs from std/sugar import `=>` From cc6157300550797f7201e174eed388f83ea2723f Mon Sep 17 00:00:00 2001 From: Timothee Cour Date: Wed, 17 Feb 2021 23:27:46 -0800 Subject: [PATCH 6/8] fix tests --- testament/testament.nim | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/testament/testament.nim b/testament/testament.nim index 8637f946413f9..1307c19efc9dd 100644 --- a/testament/testament.nim +++ b/testament/testament.nim @@ -12,7 +12,7 @@ import strutils, pegs, os, osproc, streams, json, std/exitprocs, backend, parseopt, specs, htmlgen, browsers, terminal, - algorithm, times, md5, sequtils, azure, intsets, macros + algorithm, times, md5, azure, intsets, macros from std/sugar import dup import compiler/nodejs import lib/stdtest/testutils @@ -501,7 +501,8 @@ proc testSpecHelper(r: var TResults, test: var TTest, expected: TSpec, var args = test.args if isJsTarget: exeCmd = nodejs - args = concat(@[exeFile], args) + # see D20210217T215950 + args = @["--unhandled-rejections=strict", exeFile] & args else: exeCmd = exeFile.dup(normalizeExe) if expected.useValgrind != disabled: @@ -510,6 +511,7 @@ proc testSpecHelper(r: var TResults, test: var TTest, expected: TSpec, valgrindOptions.add "--leak-check=yes" args = valgrindOptions & exeCmd & args exeCmd = "valgrind" + # xxx honor `testament --verbose` here var (_, buf, exitCode) = execCmdEx2(exeCmd, args, input = expected.input) # Treat all failure codes from nodejs as 1. Older versions of nodejs used # to return other codes, but for us it is sufficient to know that it's not 0. From 43f0f8219407b8740091ed3ece323aa104184ada Mon Sep 17 00:00:00 2001 From: Timothee Cour Date: Thu, 18 Feb 2021 00:26:58 -0800 Subject: [PATCH 7/8] fix cryptic windows error: The parameter is incorrect --- compiler/nim.nim | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/compiler/nim.nim b/compiler/nim.nim index 80a48a3cd04f8..5ec8918164862 100644 --- a/compiler/nim.nim +++ b/compiler/nim.nim @@ -96,15 +96,14 @@ proc handleCmdLine(cache: IdentCache; conf: ConfigRef) = case conf.backend of backendC, backendCpp, backendObjc: discard of backendJs: - cmdPrefix = findNodeJs() - cmdPrefix.add " --unhandled-rejections=strict" - #[ - D20210217T215950:here - this flag is needed for node < v15.0.0, otherwise `tasyncjs_fail` would fail, - refs https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode - ]# + # D20210217T215950:here this flag is needed for node < v15.0.0, otherwise + # tasyncjs_fail` would fail, refs https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode + cmdPrefix = findNodeJs() & " --unhandled-rejections=strict " else: doAssert false, $conf.backend - execExternalProgram(conf, cmdPrefix & ' ' & output.quoteShell & ' ' & conf.arguments) + # No space before command otherwise on windows you'd get a cryptic: + # `The parameter is incorrect` + execExternalProgram(conf, cmdPrefix & output.quoteShell & ' ' & conf.arguments) + # execExternalProgram(conf, cmdPrefix & ' ' & output.quoteShell & ' ' & conf.arguments) of cmdDocLike, cmdRst2html, cmdRst2tex: # bugfix(cmdRst2tex was missing) if conf.arguments.len > 0: # reserved for future use From 32c5d5aff131f216ffa06b0aa00c2ae6e76966df Mon Sep 17 00:00:00 2001 From: Timothee Cour Date: Thu, 18 Feb 2021 16:45:56 -0800 Subject: [PATCH 8/8] address comments --- changelog.md | 2 +- lib/js/asyncjs.nim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index 6a38fd0207187..6b8c8f39e4232 100644 --- a/changelog.md +++ b/changelog.md @@ -203,7 +203,7 @@ provided by the operating system. - Added `-d:nimStrictMode` in CI in several places to ensure code doesn't have certain hints/warnings -- Added `then`, `catch` to `asyncjs`. +- Added `then`, `catch` to `asyncjs`, for now hidden behind `-d:nimExperimentalAsyncjsThen`. ## Tool changes diff --git a/lib/js/asyncjs.nim b/lib/js/asyncjs.nim index 4e08f3508a21b..45053fbaa923c 100644 --- a/lib/js/asyncjs.nim +++ b/lib/js/asyncjs.nim @@ -169,7 +169,7 @@ 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 RootObj + 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