From 0f15d847d0bd8bff23f052f89e90e933d890865e Mon Sep 17 00:00:00 2001 From: duobei Date: Thu, 12 Mar 2026 11:19:27 +0800 Subject: [PATCH 1/5] feat: add wasm-gc support for timer, event loop, and time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add wasm-gc implementations that delegate to the JS host via WASM imports: - timer.wasm-gc.mbt: Timer using host-provided setTimeout/clearTimeout - event_loop.wasm-gc.mbt: cooperative scheduling via setTimeout(0) - time.mbt: ms_since_epoch via host-provided Date.now() - wasm-gc-imports.js: JS import object for the host to provide This enables @async.sleep(), @async.Timer, and @async.now() on the wasm-gc target when running in a JavaScript host (browser or Node.js). The WASM import convention uses "moonbitlang_async_timer" and "moonbitlang_async_time" as module names. Partial fix for #233 — timer/event-loop/time only; fs/process/socket remain unimplemented on wasm-gc. --- .../event_loop/event_loop.wasm-gc.mbt | 36 +++++++++++++++++++ src/internal/event_loop/moon.pkg | 5 ++- src/internal/event_loop/timer.wasm-gc.mbt | 36 +++++++++++++++++++ src/internal/event_loop/wasm-gc-imports.js | 14 ++++++++ src/internal/time/time.mbt | 29 ++++++++++++++- 5 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/internal/event_loop/event_loop.wasm-gc.mbt create mode 100644 src/internal/event_loop/timer.wasm-gc.mbt create mode 100644 src/internal/event_loop/wasm-gc-imports.js diff --git a/src/internal/event_loop/event_loop.wasm-gc.mbt b/src/internal/event_loop/event_loop.wasm-gc.mbt new file mode 100644 index 00000000..703527ab --- /dev/null +++ b/src/internal/event_loop/event_loop.wasm-gc.mbt @@ -0,0 +1,36 @@ +// Copyright 2025 International Digital Economy Academy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +///| +let _ignore_unused_import : Unit = { + ignore(@c_buffer.unimplemented) + ignore(@fd_util.unimplemented) + ignore(@os_string.unimplemented) + ignore(@os_error.unimplemented) + ignore(@time.ms_since_epoch) +} + +///| +pub fn reschedule() -> Unit { + guard !@coroutine.no_more_work() else { } + @coroutine.reschedule() + // Instead of looping blockingly until there is no ready task, + // we only perform one round of scheduling here + // (i.e., only those tasks already ready when `reschedule` is called are run). + // Remaining tasks are delayed until the next event loop iteration, + // so that those blocking jobs got a chance to execute instead of starving. + if @coroutine.has_immediately_ready_task() { + ignore(wasm_set_timeout(0, reschedule)) + } +} diff --git a/src/internal/event_loop/moon.pkg b/src/internal/event_loop/moon.pkg index a3745c74..f9f2bc9a 100644 --- a/src/internal/event_loop/moon.pkg +++ b/src/internal/event_loop/moon.pkg @@ -36,6 +36,7 @@ options( "cancel_before_submit_test.mbt": [ "native" ], "event_loop.js.mbt": [ "js" ], "event_loop.mbt": [ "native" ], + "event_loop.wasm-gc.mbt": [ "wasm-gc" ], "fs.mbt": [ "native" ], "io.mbt": [ "native" ], "io_unix.mbt": [ "native" ], @@ -49,8 +50,10 @@ options( "process_windows.mbt": [ "native" ], "stdio.mbt": [ "native" ], "thread_pool.mbt": [ "native" ], + "timer.js.mbt": [ "js" ], "timer.mbt": [ "native" ], - "unimplemented.mbt": [ "wasm", "wasm-gc" ], + "timer.wasm-gc.mbt": [ "wasm-gc" ], + "unimplemented.mbt": [ "wasm" ], "worker_wbtest.mbt": [ "native", "js" ], }, ) diff --git a/src/internal/event_loop/timer.wasm-gc.mbt b/src/internal/event_loop/timer.wasm-gc.mbt new file mode 100644 index 00000000..73895dce --- /dev/null +++ b/src/internal/event_loop/timer.wasm-gc.mbt @@ -0,0 +1,36 @@ +// Copyright 2025 International Digital Economy Academy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +///| +#external +type Timer + +///| +fn wasm_set_timeout(duration : Int, f : () -> Unit) -> Timer = "moonbitlang_async_timer" "set_timeout" + +///| +fn wasm_clear_timeout(timer : Timer) = "moonbitlang_async_timer" "clear_timeout" + +///| +pub fn Timer::new(duration : Int, f : () -> Unit) -> Timer { + wasm_set_timeout(duration, () => { + f() + reschedule() + }) +} + +///| +pub fn Timer::cancel(self : Timer) -> Unit { + wasm_clear_timeout(self) +} diff --git a/src/internal/event_loop/wasm-gc-imports.js b/src/internal/event_loop/wasm-gc-imports.js new file mode 100644 index 00000000..a2405c91 --- /dev/null +++ b/src/internal/event_loop/wasm-gc-imports.js @@ -0,0 +1,14 @@ +// Import object for moonbitlang/async wasm-gc timer and event loop. +// When loading the compiled wasm-gc module, merge this into the import object: +// +// const imports = { ...otherImports, ...asyncImports }; +// const { instance } = await WebAssembly.instantiateStreaming(fetch("module.wasm"), imports); + +export const moonbitlang_async_timer = { + set_timeout: (duration, f) => setTimeout(f, duration), + clear_timeout: (timer) => clearTimeout(timer), +}; + +export const moonbitlang_async_time = { + date_now: () => Date.now(), +}; diff --git a/src/internal/time/time.mbt b/src/internal/time/time.mbt index 74a4d4da..652dd110 100644 --- a/src/internal/time/time.mbt +++ b/src/internal/time/time.mbt @@ -62,7 +62,34 @@ pub fn ms_since_epoch() -> Int64 { } ///| -#cfg(any(target="wasm", target="wasm-gc")) +#cfg(target="wasm-gc") +fn wasm_gc_date_now() -> Double = "moonbitlang_async_time" "date_now" + +///| +/// Get the current wall-clock time in milliseconds. +/// +/// This is the time source used by the async runtime. It is intended for +/// computing elapsed time by subtraction. The value can jump forwards or +/// backwards if the system clock is adjusted, so do not assume monotonicity. +/// +/// Platform notes: +/// - Unix/macOS: `gettimeofday()` (Unix epoch) +/// - Windows: `GetSystemTimeAsFileTime()` (FILETIME epoch, 1601) +/// - JavaScript/wasm-gc: `Date.now()` (Unix epoch) +/// +/// # Example +/// ```mbt check +/// test { +/// let _ : Int64 = ms_since_epoch() +/// } +/// ``` +#cfg(target="wasm-gc") +pub fn ms_since_epoch() -> Int64 { + wasm_gc_date_now().to_int64() +} + +///| +#cfg(target="wasm") pub fn ms_since_epoch() -> Int64 { abort("unimplemented") } From a7a6d9ada9b1490e9e5d82403674d9ee534f4be2 Mon Sep 17 00:00:00 2001 From: duobei Date: Thu, 12 Mar 2026 12:00:41 +0800 Subject: [PATCH 2/5] docs: fix wasm-gc-imports.js header comment - Mention time module alongside timer and event loop - Show correct import usage with named exports --- src/internal/event_loop/wasm-gc-imports.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/internal/event_loop/wasm-gc-imports.js b/src/internal/event_loop/wasm-gc-imports.js index a2405c91..391300a2 100644 --- a/src/internal/event_loop/wasm-gc-imports.js +++ b/src/internal/event_loop/wasm-gc-imports.js @@ -1,7 +1,8 @@ -// Import object for moonbitlang/async wasm-gc timer and event loop. -// When loading the compiled wasm-gc module, merge this into the import object: +// Import objects for moonbitlang/async wasm-gc timer, event loop, and time modules. +// When loading the compiled wasm-gc module, merge these into the import object: // -// const imports = { ...otherImports, ...asyncImports }; +// import { moonbitlang_async_timer, moonbitlang_async_time } from "./wasm-gc-imports.js"; +// const imports = { ...otherImports, moonbitlang_async_timer, moonbitlang_async_time }; // const { instance } = await WebAssembly.instantiateStreaming(fetch("module.wasm"), imports); export const moonbitlang_async_timer = { From 9c46caf9017908a4193d07d4ec9d9eb1e6f6459e Mon Sep 17 00:00:00 2001 From: duobei Date: Thu, 12 Mar 2026 13:19:08 +0800 Subject: [PATCH 3/5] ci: retry flaky sanitizer-check-windows From 5fae260640853741a96bcbb78fafe1db8be5701a Mon Sep 17 00:00:00 2001 From: duobei Date: Thu, 12 Mar 2026 15:42:09 +0800 Subject: [PATCH 4/5] ci: retry From 8d0e632481e28c2e0e0beaf35de0339eddadc537 Mon Sep 17 00:00:00 2001 From: duobei Date: Tue, 17 Mar 2026 07:08:23 +0800 Subject: [PATCH 5/5] refactor: merge js/wasm-gc timer and event_loop files using #cfg - Use #cfg(target) to share Timer FFI in timer.js.mbt for both js and wasm-gc backends, replacing the duplicate timer.wasm-gc.mbt - Extend event_loop.js.mbt target to [js, wasm-gc] in moon.pkg, replacing the duplicate event_loop.wasm-gc.mbt - Rename wasm-gc FFI imports to camelCase (setTimeout/clearTimeout) to match JS native names and js backend convention - Fix comment typo: untin -> until --- src/internal/event_loop/event_loop.js.mbt | 2 +- .../event_loop/event_loop.wasm-gc.mbt | 36 ------------------- src/internal/event_loop/moon.pkg | 6 ++-- src/internal/event_loop/timer.js.mbt | 16 +++++++++ src/internal/event_loop/timer.wasm-gc.mbt | 36 ------------------- src/internal/event_loop/wasm-gc-imports.js | 4 +-- 6 files changed, 21 insertions(+), 79 deletions(-) delete mode 100644 src/internal/event_loop/event_loop.wasm-gc.mbt delete mode 100644 src/internal/event_loop/timer.wasm-gc.mbt diff --git a/src/internal/event_loop/event_loop.js.mbt b/src/internal/event_loop/event_loop.js.mbt index 407ed629..1e7d865d 100644 --- a/src/internal/event_loop/event_loop.js.mbt +++ b/src/internal/event_loop/event_loop.js.mbt @@ -28,7 +28,7 @@ pub fn reschedule() -> Unit { // Instead of looping blockingly until there is no ready task, // we only perform one round of scheduling here // (i.e., only those tasks already ready when `reschedule` is called are run). - // Remaining tasks are delayed untin the next js event loop, + // Remaining tasks are delayed until the next event loop iteration, // so that those blocking jobs got a chance to execute instead of starving. if @coroutine.has_immediately_ready_task() { ignore(set_timeout(0, reschedule)) diff --git a/src/internal/event_loop/event_loop.wasm-gc.mbt b/src/internal/event_loop/event_loop.wasm-gc.mbt deleted file mode 100644 index 703527ab..00000000 --- a/src/internal/event_loop/event_loop.wasm-gc.mbt +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2025 International Digital Economy Academy -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -///| -let _ignore_unused_import : Unit = { - ignore(@c_buffer.unimplemented) - ignore(@fd_util.unimplemented) - ignore(@os_string.unimplemented) - ignore(@os_error.unimplemented) - ignore(@time.ms_since_epoch) -} - -///| -pub fn reschedule() -> Unit { - guard !@coroutine.no_more_work() else { } - @coroutine.reschedule() - // Instead of looping blockingly until there is no ready task, - // we only perform one round of scheduling here - // (i.e., only those tasks already ready when `reschedule` is called are run). - // Remaining tasks are delayed until the next event loop iteration, - // so that those blocking jobs got a chance to execute instead of starving. - if @coroutine.has_immediately_ready_task() { - ignore(wasm_set_timeout(0, reschedule)) - } -} diff --git a/src/internal/event_loop/moon.pkg b/src/internal/event_loop/moon.pkg index f9f2bc9a..ce30c5f9 100644 --- a/src/internal/event_loop/moon.pkg +++ b/src/internal/event_loop/moon.pkg @@ -34,9 +34,8 @@ options( ], targets: { "cancel_before_submit_test.mbt": [ "native" ], - "event_loop.js.mbt": [ "js" ], + "event_loop.js.mbt": [ "js", "wasm-gc" ], "event_loop.mbt": [ "native" ], - "event_loop.wasm-gc.mbt": [ "wasm-gc" ], "fs.mbt": [ "native" ], "io.mbt": [ "native" ], "io_unix.mbt": [ "native" ], @@ -50,9 +49,8 @@ options( "process_windows.mbt": [ "native" ], "stdio.mbt": [ "native" ], "thread_pool.mbt": [ "native" ], - "timer.js.mbt": [ "js" ], + "timer.js.mbt": [ "js", "wasm-gc" ], "timer.mbt": [ "native" ], - "timer.wasm-gc.mbt": [ "wasm-gc" ], "unimplemented.mbt": [ "wasm" ], "worker_wbtest.mbt": [ "native", "js" ], }, diff --git a/src/internal/event_loop/timer.js.mbt b/src/internal/event_loop/timer.js.mbt index 3d3d4fc3..a5e75a5d 100644 --- a/src/internal/event_loop/timer.js.mbt +++ b/src/internal/event_loop/timer.js.mbt @@ -17,9 +17,14 @@ type Timer ///| +#cfg(target="js") extern "js" fn set_timeout(duration : Int, f : () -> Unit) -> Timer = #| (duration, f) => setTimeout(f, duration) +///| +#cfg(target="wasm-gc") +fn set_timeout(duration : Int, f : () -> Unit) -> Timer = "moonbitlang_async_timer" "setTimeout" + ///| pub fn Timer::new(duration : Int, f : () -> Unit) -> Timer { set_timeout(duration, () => { @@ -29,5 +34,16 @@ pub fn Timer::new(duration : Int, f : () -> Unit) -> Timer { } ///| +#cfg(target="js") pub extern "js" fn Timer::cancel(self : Timer) = #| (timer) => clearTimeout(timer) + +///| +#cfg(target="wasm-gc") +fn clear_timeout(timer : Timer) = "moonbitlang_async_timer" "clearTimeout" + +///| +#cfg(target="wasm-gc") +pub fn Timer::cancel(self : Timer) -> Unit { + clear_timeout(self) +} diff --git a/src/internal/event_loop/timer.wasm-gc.mbt b/src/internal/event_loop/timer.wasm-gc.mbt deleted file mode 100644 index 73895dce..00000000 --- a/src/internal/event_loop/timer.wasm-gc.mbt +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2025 International Digital Economy Academy -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -///| -#external -type Timer - -///| -fn wasm_set_timeout(duration : Int, f : () -> Unit) -> Timer = "moonbitlang_async_timer" "set_timeout" - -///| -fn wasm_clear_timeout(timer : Timer) = "moonbitlang_async_timer" "clear_timeout" - -///| -pub fn Timer::new(duration : Int, f : () -> Unit) -> Timer { - wasm_set_timeout(duration, () => { - f() - reschedule() - }) -} - -///| -pub fn Timer::cancel(self : Timer) -> Unit { - wasm_clear_timeout(self) -} diff --git a/src/internal/event_loop/wasm-gc-imports.js b/src/internal/event_loop/wasm-gc-imports.js index 391300a2..7f70a582 100644 --- a/src/internal/event_loop/wasm-gc-imports.js +++ b/src/internal/event_loop/wasm-gc-imports.js @@ -6,8 +6,8 @@ // const { instance } = await WebAssembly.instantiateStreaming(fetch("module.wasm"), imports); export const moonbitlang_async_timer = { - set_timeout: (duration, f) => setTimeout(f, duration), - clear_timeout: (timer) => clearTimeout(timer), + setTimeout: (duration, f) => setTimeout(f, duration), + clearTimeout: (timer) => clearTimeout(timer), }; export const moonbitlang_async_time = {