From dc904bfdd17ed1108580367b34bbe7204ed4ea95 Mon Sep 17 00:00:00 2001
From: Cheng Shao <terrorjack@type.dance>
Date: Sat, 15 Mar 2025 06:29:17 +0000
Subject: [PATCH] wasm: implement promise.throwTo() for async JSFFI exports

This commit implements promise.throwTo() for wasm backend JSFFI
exports. This allows the JavaScript side to interrupt Haskell
computation by raising an async exception. See subsequent docs/test
commits for more details.
---
 .../src/GHC/Internal/Wasm/Prim/Exports.hs     | 18 ++++++++++++---
 rts/wasm/JSFFI.c                              | 23 +++++++++++++++++++
 2 files changed, 38 insertions(+), 3 deletions(-)

diff --git a/libraries/ghc-internal/src/GHC/Internal/Wasm/Prim/Exports.hs b/libraries/ghc-internal/src/GHC/Internal/Wasm/Prim/Exports.hs
index 1c02795b92d..c25c9f8fc7c 100644
--- a/libraries/ghc-internal/src/GHC/Internal/Wasm/Prim/Exports.hs
+++ b/libraries/ghc-internal/src/GHC/Internal/Wasm/Prim/Exports.hs
@@ -34,6 +34,7 @@ import GHC.Internal.Base
 import GHC.Internal.Exception.Type
 import GHC.Internal.Exts
 import GHC.Internal.IO
+import GHC.Internal.IORef
 import GHC.Internal.Int
 import GHC.Internal.Stable
 import GHC.Internal.TopHandler (flushStdHandles)
@@ -65,9 +66,14 @@ runIO res m = do
         let tmp@(JSString tmp_v) = toJSString $ displayException err
         js_promiseReject p tmp
         freeJSVal tmp_v
-  IO $ \s0 -> case fork# (unIO $ catch (res p =<< m) topHandler *> flushStdHandles) s0 of
-    (# s1, _ #) -> case stg_scheduler_loop# s1 of
-      (# s2, _ #) -> (# s2, p #)
+  post_action_ref <- newIORef $ pure ()
+  IO $ \s0 -> case fork# (unIO $ catch (res p =<< m) topHandler *> flushStdHandles *> join (readIORef post_action_ref)) s0 of
+    (# s1, tso# #) -> case mkWeakNoFinalizer# tso# () s1 of
+      (# s2, w# #) -> case makeStablePtr# w# s2 of
+        (# s3, sp# #) -> case unIO (writeIORef post_action_ref $ js_promiseDelThrowTo p *> freeStablePtr (StablePtr $ unsafeCoerce# sp#)) s3 of
+          (# s4, _ #) -> case unIO (js_promiseAddThrowTo p $ StablePtr $ unsafeCoerce# sp#) s4 of
+            (# s5, _ #) -> case stg_scheduler_loop# s5 of
+              (# s6, _ #) -> (# s6, p #)
 
 runNonIO :: (JSVal -> a -> IO ()) -> a -> IO JSVal
 runNonIO res a = runIO res $ pure a
@@ -75,6 +81,12 @@ runNonIO res a = runIO res $ pure a
 foreign import javascript unsafe "let res, rej; const p = new Promise((resolve, reject) => { res = resolve; rej = reject; }); p.resolve = res; p.reject = rej; return p;"
   js_promiseWithResolvers :: IO JSVal
 
+foreign import javascript unsafe "$1.throwTo = (err) => __exports.rts_promiseThrowTo($2, err);"
+  js_promiseAddThrowTo :: JSVal -> StablePtr Any -> IO ()
+
+foreign import javascript unsafe "$1.throwTo = () => {};"
+  js_promiseDelThrowTo :: JSVal -> IO ()
+
 foreign import prim "stg_scheduler_loopzh"
   stg_scheduler_loop# :: State# RealWorld -> (# State# RealWorld, () #)
 
diff --git a/rts/wasm/JSFFI.c b/rts/wasm/JSFFI.c
index 0c298b064a1..a1a52d676f4 100644
--- a/rts/wasm/JSFFI.c
+++ b/rts/wasm/JSFFI.c
@@ -1,6 +1,8 @@
 #include "Rts.h"
 #include "Prelude.h"
+#include "RaiseAsync.h"
 #include "Schedule.h"
+#include "Threads.h"
 #include "sm/Sanity.h"
 
 #if defined(__wasm_reference_types__)
@@ -212,6 +214,27 @@ void rts_promiseReject(HsStablePtr, HsJSVal);
 void rts_promiseReject(HsStablePtr sp, HsJSVal js_err)
   mk_rtsPromiseCallback(rts_apply(cap, &ghczminternal_GHCziInternalziWasmziPrimziImports_raiseJSException_closure, rts_mkJSVal(cap, js_err)))
 
+__attribute__((export_name("rts_promiseThrowTo")))
+void rts_promiseThrowTo(HsStablePtr, HsJSVal);
+void rts_promiseThrowTo(HsStablePtr sp, HsJSVal js_err) {
+  Capability *cap = &MainCapability;
+  StgWeak *w = (StgWeak *)deRefStablePtr(sp);
+  if (w->header.info == &stg_DEAD_WEAK_info) {
+    return;
+  }
+  ASSERT(w->header.info == &stg_WEAK_info);
+  StgTSO *tso = (StgTSO *)w->key;
+  ASSERT(tso->header.info == &stg_TSO_info);
+  throwToSelf(
+      cap, tso,
+      rts_apply(
+          cap,
+          &ghczminternal_GHCziInternalziWasmziPrimziImports_raiseJSException_closure,
+          rts_mkJSVal(cap, js_err)));
+  tryWakeupThread(cap, tso);
+  rts_schedulerLoop();
+}
+
 __attribute__((export_name("rts_freeStablePtr")))
 void rts_freeStablePtr(HsStablePtr);
 void rts_freeStablePtr(HsStablePtr sp) {
-- 
GitLab