From 63e1cba08a22eccfeb7489ca9706ecc8ccf11640 Mon Sep 17 00:00:00 2001 From: Guset0x0 Date: Fri, 10 Jan 2025 14:52:12 +0800 Subject: [PATCH] add document for experimental async programming support --- .github/workflows/next-check.yml | 6 ++ next/language/async-experimental.md | 80 +++++++++++++++ next/language/index.md | 1 + next/sources/async/moon.mod.json | 10 ++ next/sources/async/src/async.mbt | 120 ++++++++++++++++++++++ next/sources/async/src/moon.pkg.json | 3 + next/sources/language/src/async/async.mbt | 91 ++++++++++++++++ 7 files changed, 311 insertions(+) create mode 100644 next/language/async-experimental.md create mode 100644 next/sources/async/moon.mod.json create mode 100644 next/sources/async/src/async.mbt create mode 100644 next/sources/async/src/moon.pkg.json create mode 100644 next/sources/language/src/async/async.mbt diff --git a/.github/workflows/next-check.yml b/.github/workflows/next-check.yml index 208633e3..3b4db3dc 100644 --- a/.github/workflows/next-check.yml +++ b/.github/workflows/next-check.yml @@ -50,6 +50,9 @@ jobs: for dir in next/sources/*; do if [ -d "$dir" ]; then echo "Processing $dir" + if [ "$dir" = next/sources/async && ${{ matrix.backend }} != "js" ]; then + continue + fi if ! (cd "$dir" && moon install && moon check --deny-warn --target ${{ matrix.backend }} && moon test --target ${{ matrix.backend }}); then echo "Failed in $dir" failed_directories+=("$dir") @@ -77,6 +80,9 @@ jobs: $failed_directories = @() Get-ChildItem -Path ".\next\sources" -Directory | ForEach-Object { Write-Output "Processing $($_.FullName)" + if ($_.Name -eq "async" && ${{ matrix.backend }} -ne "js") { + return + } Set-Location $_.FullName moon install && moon check --deny-warn --target ${{ matrix.backend }} && moon test --target ${{ matrix.backend }} if (!$?) { diff --git a/next/language/async-experimental.md b/next/language/async-experimental.md new file mode 100644 index 00000000..2e41e43a --- /dev/null +++ b/next/language/async-experimental.md @@ -0,0 +1,80 @@ +# Experimental async programming support + +MoonBit is providing experimental support for async programming. +But the design and API is still highly unstable, and may receive big breaking change in the future. +This page documents the current design, and we highly appreciate any feedback or experiment with current design. + +## Async function +Async functions can be declared with the `async` keyword: + +```{literalinclude} /sources/language/src/async/async.mbt +:language: moonbit +:start-after: start async function declaration +:end-before: end async function declaration +``` + +Async functions must be called with the `!!` operator: + +```{literalinclude} /sources/language/src/async/async.mbt +:language: moonbit +:start-after: start async function call syntax +:end-before: end async function syntax +``` + +If the async function may throw error, `!!` will also rethrow the error. + +## Async primitives for suspension +MoonBit provides two core primitives for `%async.suspend` and `%async.run`: + +```{literalinclude} /sources/async/src/async.mbt +:language: moonbit +:start-after: start async primitive +:end-before: end async primitive +``` + +There two primitives are not intended for direct use by end users. +However, since MoonBit's standard library for async programming is still under development, +currently users need to bind these two primitives manually to do async programming. + +There are two ways of reading these primitives: + +- the coroutine reading: `%async.run` spawn a new corourtine, + and `%async.suspend` suspend current coroutine. + The main difference with other languages here is: + instead of yielding all the way to the caller of `%async.run`, + resumption of the coroutine is handled by the callback passed to `%async.suspend` +- the delimited continuation reading: `%async.run` is the `reset` operator in delimited continuation, + and `%async.suspend` is the `shift` operator in delimited continuation + +Here's an example of how these two primitives work: + +```{literalinclude} /sources/async/src/async.mbt +:language: moonbit +:start-after: start async example +:end-before: end async example +``` + +In `async_worker`, `suspend` will capture the rest of the current coroutine as two "continuation" functions, and pass them to a callback. +In the callback, calling `resume_ok` will resume execution at the point of `suspend!!(...)`, +all the way until the `run_async` call that start this coroutine. +calling `resume_err` will also resume execution of current coroutine, +but it will make `suspend!!(...)` throw an error instead of returning normally. + +Notice that `suspend` type may throw error, even if `suspend` itself never throw an error directly. +This design makes coroutines cancellable at every `suspend` call: just call the corresponding `resume_err` callback. + +## Integrating with JS Promise/callback based API +Since MoonBit's standard async library is still under development, +so there is no ready-to-use implementation for event loop and IO operations yet. +So the easiest way to write some async program is to use MoonBit's Javascript backend, +and reuse the event loop and IO operation sof Javascript. +Here's an example of integrating MoonBit's async programming support wtih JS's callback based API: + +```{literalinclude} /sources/async/src/async.mbt +:language: moonbit +:start-after: start async timer example +:end-before: end async timer example +``` + +Integrating with JS Promise is easy too: +just pass `resume_ok` as the `resolve` callback and `resume_err` as the `reject` callback to a JS promise. diff --git a/next/language/index.md b/next/language/index.md index 215012cd..756917aa 100644 --- a/next/language/index.md +++ b/next/language/index.md @@ -26,4 +26,5 @@ tests docs ffi-and-wasm-host derive +async-experimental ``` diff --git a/next/sources/async/moon.mod.json b/next/sources/async/moon.mod.json new file mode 100644 index 00000000..ae3c49d6 --- /dev/null +++ b/next/sources/async/moon.mod.json @@ -0,0 +1,10 @@ +{ + "name": "moonbit-community/async-doc", + "version": "0.1.0", + "readme": "README.md", + "repository": "", + "license": "Apache-2.0", + "keywords": [], + "description": "", + "source": "src" +} diff --git a/next/sources/async/src/async.mbt b/next/sources/async/src/async.mbt new file mode 100644 index 00000000..bce30722 --- /dev/null +++ b/next/sources/async/src/async.mbt @@ -0,0 +1,120 @@ +// start async function declaration +async fn my_async_function() -> Unit { + ... +} + +// anonymous/local function +test { + let async_lambda = async fn () { + ... + } + async fn local_async_function() { + ... + } +} +// end async function declaration + +// start async function call syntax +async fn my_async_function() -> Unit! { + ... +} + +fn another_async_function() -> Unit! { + // error will be rethrowed by `!!` + my_async_function!!() +} +// end async function call syntax + +// start async primitive + +// `run_async` spawn a new coroutine and execute an async function in it +fn run_async(f : async () -> Unit) -> Unit = "%async.run" + +// `suspend` will suspend the execution of the current coroutine. +// The suspension will be handled by a callback passed to `suspend` +async fn suspend[T, E : Error]( + // `f` is a callback for handling suspension + f : ( + // the first parameter of `f` is used to resume the execution of the coroutine normally + (T) -> Unit, + // the second parameter of `f` is used to cancel the execution of the current coroutine + // by throwing an error at suspension point + (E) -> Unit + ) -> Unit +) -> T!E +// end async primitive + +// start async example +type! MyError derive(Show) + +async fn async_worker(throw_error~ : Bool) -> Unit!MyError { + suspend!!(fn (resume_ok, resume_err) { + if throw_error { + resume_err(MyError) + } else { + resume_ok(()) + println("the end of the coroutine") + } + }) +} + +// the program above should print: +// +// the worker finishes +// the end of the coroutine +// after the first coroutine finishes +// catched MyError +test { + // when supplying an anonymous function + // to a higher order function that expects async parameter, + // the `async` keyword can be omitted + run_async(fn () { + try { + async_worker!!(throw_error=false) + println("the worker finishes") + } catch { + err => println("catched: \{err}") + } + }) + println("after the first coroutine finishes") + run_async(fn () { + try { + async_worker!!(throw_error=true) + println("this message should be printed after the worker finishes") + } catch { + err => println("catched: \{err}") + } + }) +} +// end async example + +// start async timer example +type JSTimer +extern "js" fn js_set_timeout(f : () -> Unit, duration : Int) -> JSTimer = + #| (f, duration) => setTimeout(f, duration) + +async fn sleep(duration : Int) -> Unit! { + suspend!!(fn (resume_ok, _resume_err) { + let _ = setTimeout(resume_ok, duration) + }) +} + +test { + run_async(fn () { + sleep!!(500) + println("timer 1 tick") + sleep!!(1000) + println("timer 1 tick") + sleep!!(1500) + println("timer 1 tick") + }) + run_async(fn () { + sleep!!(600) + println("timer 2 tick") + sleep!!(600) + println("timer 2 tick") + sleep!!(600) + println("timer 2 tick") + }) +} +// end async timer example diff --git a/next/sources/async/src/moon.pkg.json b/next/sources/async/src/moon.pkg.json new file mode 100644 index 00000000..e5020f94 --- /dev/null +++ b/next/sources/async/src/moon.pkg.json @@ -0,0 +1,3 @@ +{ + "warn-list": "-6" +} diff --git a/next/sources/language/src/async/async.mbt b/next/sources/language/src/async/async.mbt new file mode 100644 index 00000000..ff5ce5d0 --- /dev/null +++ b/next/sources/language/src/async/async.mbt @@ -0,0 +1,91 @@ +// start async function declaration +async fn my_async_function() -> Unit { + ... +} + +// anonymous/local function +fn main { + let async_lambda = async fn () { + ... + } + async fn local_async_function() { + ... + } +} +// end async function declaration + +// start async function call syntax +async fn my_async_function() -> Unit! { + ... +} + +fn another_async_function() -> Unit! { + // error will be rethrowed by `!!` + my_async_function!!() +} +// end async function call syntax + +// start async primitive + +// `run_async` spawn a new coroutine and execute an async function in it +fn run_async(f : async () -> Unit) -> Unit = "%async.run" + +// `suspend` will suspend the execution of the current coroutine. +// The suspension will be handled by a callback passed to `suspend` +async fn suspend[T, E : Error]( + // `f` is a callback for handling suspension + f : ( + // the first parameter of `f` is used to resume the execution of the coroutine normally + (T) -> Unit, + // the second parameter of `f` is used to cancel the execution of the current coroutine + // by throwing an error at suspension point + (E) -> Unit + ) -> Unit +) -> T!E +// end async primitive + +// start async example +fn run_async(f : async () -> Unit) -> Unit = "%async.run" +async fn suspend[T, E : Error]( + f : ((T) -> Unit, (E) -> Unit) -> Unit +) -> T!E + +type! MyError derive(Show) + +async fn async_worker(throw_error~ : Bool) -> Unit!MyError { + suspend!!(fn (resume_ok, resume_err) { + if throw_error { + resume_err(MyError) + } else { + resume_ok(()) + println("the end of the coroutine") + } + }) +} + +// the program above should print: +// +// the worker finishes +// the end of the coroutine +// after the first coroutine finishes +// catched MyError +fn main { + run_async(fn () { + try { + async_worker!!(throw_error=false) + println("the worker finishes") + } catch { + err => println("catched: \{err}") + } + }) + println("after the first coroutine finishes") + run_async(fn () { + try { + async_worker!!(throw_error=true) + println("this message should be printed after the worker finishes") + } catch { + err => println("catched: \{err}") + } + }) +} +// end async example