diff --git a/.github/workflows/next-check.yml b/.github/workflows/next-check.yml index 208633e3..49e08cab 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 [ $(basename "$dir") = 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..654818fb --- /dev/null +++ b/next/language/async-experimental.md @@ -0,0 +1,82 @@ +# 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/async/src/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/async/src/async.mbt +:language: moonbit +:start-after: start async function call syntax +:end-before: end async function call syntax +``` + +If the async function may throw error, `!!` will also rethrow the error. + +Async functions can only be called in async functions. Currently, async functions cannot be called in the body of `for .. in` loops. + +## 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 coroutine, + 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 operations of Javascript. +Here's an example of integrating MoonBit's async programming support with 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/locales/zh_CN/LC_MESSAGES/language.po b/next/locales/zh_CN/LC_MESSAGES/language.po index ca963c87..2043229d 100644 --- a/next/locales/zh_CN/LC_MESSAGES/language.po +++ b/next/locales/zh_CN/LC_MESSAGES/language.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: MoonBit Document \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-03 11:03+0800\n" +"POT-Creation-Date: 2025-01-10 17:54+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: zh_CN\n" @@ -19,6 +19,383 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.16.0\n" +#: ../../language/async-experimental.md:1 +msgid "Experimental async programming support" +msgstr "实验性的异步编程支持" + +#: ../../language/async-experimental.md:3 +msgid "" +"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." +msgstr "" +"MoonBit 目前提供了实验性的异步编程支持。但异步相关功能的设计和 API 非常不稳定,未来可能会有大的破坏性改动。本文档页面将介绍 " +"MoonBit 异步编程目前的设计,我们欢迎及感谢任何对目前设计的反馈与使用体验分享。" + +#: ../../language/async-experimental.md:7 +msgid "Async function" +msgstr "异步函数" + +#: ../../language/async-experimental.md:8 +msgid "Async functions can be declared with the `async` keyword:" +msgstr "异步函数可以用 `async` 关键字声明:" + +#: ../../language/async-experimental.md:10 +msgid "" +"async fn my_async_function() -> Unit {\n" +" ...\n" +"}\n" +"\n" +"// anonymous/local function\n" +"test {\n" +" let async_lambda = async fn () {\n" +" ...\n" +" }\n" +" async fn local_async_function() {\n" +" ...\n" +" }\n" +"}\n" +msgstr "" +"async fn my_async_function() -> Unit {\n" +" ...\n" +"}\n" +"\n" +"// 匿名/本地函数\n" +"test {\n" +" let async_lambda = async fn () {\n" +" ...\n" +" }\n" +" async fn local_async_function() {\n" +" ...\n" +" }\n" +"}\n" + +#: ../../language/async-experimental.md:16 +msgid "Async functions must be called with the `!!` operator:" +msgstr "调用异步函数时,必须用 `!!` 操作符来标记这是一次异步函数调用:" + +#: ../../language/async-experimental.md:18 +msgid "" +"async fn some_async_function() -> Unit! {\n" +" ...\n" +"}\n" +"\n" +"async fn another_async_function() -> Unit! {\n" +" // error will be rethrowed by `!!`\n" +" some_async_function!!()\n" +"}\n" +msgstr "" +"async fn some_async_function() -> Unit! {\n" +" ...\n" +"}\n" +"\n" +"async fn another_async_function() -> Unit! {\n" +" // 异步函数中的错误也会被 `!!` 转发\n" +" some_async_function!!()\n" +"}\n" + +#: ../../language/async-experimental.md:24 +msgid "If the async function may throw error, `!!` will also rethrow the error." +msgstr "如果异步函数会抛出错误,`!!` 也会把错误一并转发。" + +#: ../../language/async-experimental.md:26 +msgid "" +"Async functions can only be called in async functions. Currently, async " +"functions cannot be called in the body of `for .. in` loops." +msgstr "异步函数只能在其他异步函数中被调用。目前,在 `for .. in` 循环中不能使用异步函数。" + +#: ../../language/async-experimental.md:28 +msgid "Async primitives for suspension" +msgstr "用于中断异步函数的原语" + +#: ../../language/async-experimental.md:29 +msgid "" +"MoonBit provides two core primitives for `%async.suspend` and " +"`%async.run`:" +msgstr "MoonBit 提供了两个用于异步编程的原语:`%async.suspend` 和 `%async.run`:" + +#: ../../language/async-experimental.md:31 +msgid "" +"\n" +"// `run_async` spawn a new coroutine and execute an async function in it\n" +"fn run_async(f : async () -> Unit) -> Unit = \"%async.run\"\n" +"\n" +"// `suspend` will suspend the execution of the current coroutine.\n" +"// The suspension will be handled by a callback passed to `suspend`\n" +"async fn suspend[T, E : Error](\n" +" // `f` is a callback for handling suspension\n" +" f : (\n" +" // the first parameter of `f` is used to resume the execution of the " +"coroutine normally\n" +" (T) -> Unit,\n" +" // the second parameter of `f` is used to cancel the execution of the" +" current coroutine\n" +" // by throwing an error at suspension point\n" +" (E) -> Unit\n" +" ) -> Unit\n" +") -> T!E = \"%async.suspend\"\n" +msgstr "" +"\n" +"// `run_async` 会创建一个新的协程,并在其中运行一个异步函数\n" +"fn run_async(f : async () -> Unit) -> Unit = \"%async.run\"\n" +"\n" +"// `suspend` 会中断当前协程的运行。\n" +"// `suspend` 会接受一个回调函数,并让这个回调函数来操作中断的协程\n" +"async fn suspend[T, E : Error](\n" +" // `f` 是负责操作中断的协程的回调函数\n" +" f : (\n" +" // `f` 的第一个参数用于继续运行被中断的协程\n" +" (T) -> Unit,\n" +" // `f` 的第二个参数用于取消被中断的协程。\n" +" // 取消会被表示为在中断处抛出错误\n" +" (E) -> Unit\n" +" ) -> Unit\n" +") -> T!E = \"%async.suspend\"\n" + +#: ../../language/async-experimental.md:37 +msgid "" +"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." +msgstr "这两个原语不应该让终端用户直接调用。但由于 MoonBit 的异步标准库仍在开发中,目前,用户需要手动绑定这两个原语,才能编写异步程序。" + +#: ../../language/async-experimental.md:41 +msgid "There are two ways of reading these primitives:" +msgstr "可以用两种不同的方式来理解这两个原语:" + +#: ../../language/async-experimental.md:43 +msgid "" +"the coroutine reading: `%async.run` spawn a new coroutine, 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`" +msgstr "" +"理解为协程:`%async.run` 创建一个新的协程,`%async.suspend` " +"中断当前协程。和其他语言的协程的主要区别是:中断协程时,不是由创建协程的地方来负责恢复执行,而是在中断的地方通过一个回调函数就地处理中断后的协程" + +#: ../../language/async-experimental.md:48 +msgid "" +"the delimited continuation reading: `%async.run` is the `reset` operator " +"in delimited continuation, and `%async.suspend` is the `shift` operator " +"in delimited continuation" +msgstr "" +"理解为 delimited continuation:`%async.run` 是 delimited continuation 中的 " +"`reset` 操作符,`%async.suspend` 是 delimited continuation `shift` 操作符" + +#: ../../language/async-experimental.md:51 +msgid "Here's an example of how these two primitives work:" +msgstr "以下是使用这两个原语的示例:" + +#: ../../language/async-experimental.md:53 +#, fuzzy +msgid "" +"type! MyError derive(Show)\n" +"\n" +"async fn async_worker(throw_error~ : Bool) -> Unit!MyError {\n" +" suspend!!(fn (resume_ok, resume_err) {\n" +" if throw_error {\n" +" resume_err(MyError)\n" +" } else {\n" +" resume_ok(())\n" +" println(\"the end of the coroutine\")\n" +" }\n" +" })\n" +"}\n" +"\n" +"// the program below should print:\n" +"//\n" +"// the worker finishes\n" +"// the end of the coroutine\n" +"// after the first coroutine finishes\n" +"// caught MyError\n" +"test {\n" +" // when supplying an anonymous function\n" +" // to a higher order function that expects async parameter,\n" +" // the `async` keyword can be omitted\n" +" run_async(fn () {\n" +" try {\n" +" async_worker!!(throw_error=false)\n" +" println(\"the worker finishes\")\n" +" } catch {\n" +" err => println(\"caught: \\{err}\")\n" +" }\n" +" })\n" +" println(\"after the first coroutine finishes\")\n" +" run_async(fn () {\n" +" try {\n" +" async_worker!!(throw_error=true)\n" +" println(\"the worker finishes\")\n" +" } catch {\n" +" err => println(\"caught: \\{err}\")\n" +" }\n" +" })\n" +"}\n" +msgstr "" +"type! MyError derive(Show)\n" +"\n" +"async fn async_worker(throw_error~ : Bool) -> Unit!MyError {\n" +" suspend!!(fn (resume_ok, resume_err) {\n" +" if throw_error {\n" +" resume_err(MyError)\n" +" } else {\n" +" resume_ok(())\n" +" println(\"协程结束运行\")\n" +" }\n" +" })\n" +"}\n" +"\n" +"// 下面这段程序应当输出:\n" +"//\n" +"// worker 函数返回了\n" +"// 协程结束运行\n" +"// 协程结束之后\n" +"// 捕获到了 MyError\n" +"test {\n" +" // 在调用一个需要异步参数的高阶函数时,\n" +" // 如果参数是一个匿名函数,可以省略 `async` 关键字\n" +" run_async(fn () {\n" +" try {\n" +" async_worker!!(throw_error=false)\n" +" println(\"worker 函数返回了\")\n" +" } catch {\n" +" err => println(\"捕获到了 \\{err}\")\n" +" }\n" +" })\n" +" println(\"协程结束之后\")\n" +" run_async(fn () {\n" +" try {\n" +" async_worker!!(throw_error=true)\n" +" println(\"worker 函数返回了\")\n" +" } catch {\n" +" err => println(\"捕获到了 \\{err}\")\n" +" }\n" +" })\n" +"}\n" + +#: ../../language/async-experimental.md:59 +msgid "" +"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." +msgstr "" +"在 `async_worker` 里,`suspend` 会捕获当前协程剩下的部分,并将它们表示成两个函数,传递给 `suspend` 的参数。在" +" `suspend` 的参数里,调用 `resume_ok` 会让 `suspend!!(...)` " +"正常返回,恢复协程的运行,一直运行到创建这个协程的 `run_async(...)` 为止。调用 `resume_err` " +"也会恢复协程的运行,但它会在 `suspend!!(...)` 的位置抛出一个错误。" + +#: ../../language/async-experimental.md:65 +msgid "" +"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." +msgstr "" +"`suspend` 的类型表明它可能抛出错误。但 `suspend` " +"自身不会直接产生任何错误。这一设计保证了协程在每一个的中断点都是可以取消的:调用对应的 `resume_err` 函数即可。" + +#: ../../language/async-experimental.md:68 +msgid "Integrating with JS Promise/callback based API" +msgstr "和 JS 的 Promise/回调 API 整合" + +#: ../../language/async-experimental.md:69 +msgid "" +"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 operations of " +"Javascript. Here's an example of integrating MoonBit's async programming " +"support with JS's callback based API:" +msgstr "" +"MoonBit 的异步标准库仍在开发中,因此,目前没有直接可用的事件循环和输入输出原语实现。目前,要使用 MoonBit " +"编写异步程序最简单的办法是使用 JS 后端,并复用 JavaScript 的事件循环和输入输出 API。下面是一个整合 MoonBit " +"的异步编程支持和 JS 的回调 API 的例子:" + +#: ../../language/async-experimental.md:75 +msgid "" +"type JSTimer\n" +"extern \"js\" fn js_set_timeout(f : () -> Unit, duration : Int) -> " +"JSTimer =\n" +" #| (f, duration) => setTimeout(f, duration)\n" +"\n" +"async fn sleep(duration : Int) -> Unit! {\n" +" suspend!!(fn (resume_ok, _resume_err) {\n" +" let _ = js_set_timeout(fn () { resume_ok(()) }, duration)\n" +" })\n" +"}\n" +"\n" +"test {\n" +" run_async(fn () {\n" +" try {\n" +" sleep!!(500)\n" +" println(\"timer 1 tick\")\n" +" sleep!!(1000)\n" +" println(\"timer 1 tick\")\n" +" sleep!!(1500)\n" +" println(\"timer 1 tick\")\n" +" } catch { _ => panic() }\n" +" })\n" +" run_async(fn () {\n" +" try {\n" +" sleep!!(600)\n" +" println(\"timer 2 tick\")\n" +" sleep!!(600)\n" +" println(\"timer 2 tick\")\n" +" sleep!!(600)\n" +" println(\"timer 2 tick\")\n" +" } catch { _ => panic() }\n" +" })\n" +"}\n" +msgstr "" +"type JSTimer\n" +"extern \"js\" fn js_set_timeout(f : () -> Unit, duration : Int) -> " +"JSTimer =\n" +" #| (f, duration) => setTimeout(f, duration)\n" +"\n" +"async fn sleep(duration : Int) -> Unit! {\n" +" suspend!!(fn (resume_ok, _resume_err) {\n" +" let _ = js_set_timeout(fn () { resume_ok(()) }, duration)\n" +" })\n" +"}\n" +"\n" +"test {\n" +" run_async(fn () {\n" +" try {\n" +" sleep!!(500)\n" +" println(\"timer 1 tick\")\n" +" sleep!!(1000)\n" +" println(\"timer 1 tick\")\n" +" sleep!!(1500)\n" +" println(\"timer 1 tick\")\n" +" } catch { _ => panic() }\n" +" })\n" +" run_async(fn () {\n" +" try {\n" +" sleep!!(600)\n" +" println(\"timer 2 tick\")\n" +" sleep!!(600)\n" +" println(\"timer 2 tick\")\n" +" sleep!!(600)\n" +" println(\"timer 2 tick\")\n" +" } catch { _ => panic() }\n" +" })\n" +"}\n" + +#: ../../language/async-experimental.md:81 +msgid "" +"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." +msgstr "" +"和 JS Promise 也非常简单:只需要把 `resume_ok` 函数用作 `Promise` 的 `resolve` 把 " +"`resume_err` 用作 Promise 的 `reject` 回调即可。" + #: ../../language/derive.md:1 msgid "Deriving traits" msgstr "派生内建特征" 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..4282838d --- /dev/null +++ b/next/sources/async/src/async.mbt @@ -0,0 +1,124 @@ +// 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 some_async_function() -> Unit! { + ... +} + +async fn another_async_function() -> Unit! { + // error will be rethrowed by `!!` + some_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 = "%async.suspend" +// 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 below should print: +// +// the worker finishes +// the end of the coroutine +// after the first coroutine finishes +// caught 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("caught: \{err}") + } + }) + println("after the first coroutine finishes") + run_async(fn () { + try { + async_worker!!(throw_error=true) + println("the worker finishes") + } catch { + err => println("caught: \{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 _ = js_set_timeout(fn () { resume_ok(()) }, duration) + }) +} + +test { + run_async(fn () { + try { + sleep!!(500) + println("timer 1 tick") + sleep!!(1000) + println("timer 1 tick") + sleep!!(1500) + println("timer 1 tick") + } catch { _ => panic() } + }) + run_async(fn () { + try { + sleep!!(600) + println("timer 2 tick") + sleep!!(600) + println("timer 2 tick") + sleep!!(600) + println("timer 2 tick") + } catch { _ => panic() } + }) +} +// 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..57edf991 --- /dev/null +++ b/next/sources/async/src/moon.pkg.json @@ -0,0 +1,3 @@ +{ + "warn-list": "-1-2-13-28" +}