From f3bfe31e6190d4161b0225e1e8bd0c8088f2c54d Mon Sep 17 00:00:00 2001
From: Cheng Shao <terrorjack@type.dance>
Date: Fri, 15 Nov 2024 03:33:55 +0000
Subject: [PATCH] wasm: add Note [Variable passing in JSFFI] as !13583 follow
 up

This patch adds a note to explain how the magic variables like
`__ghc_wasm_jsffi_dyld` are brought into scope of JSFFI code snippets,
as follow up work of !13583.
---
 libraries/ghci/GHCi/ObjLink.hs |  3 +++
 utils/jsffi/dyld.mjs           |  3 ++-
 utils/jsffi/post-link.mjs      | 41 ++++++++++++++++++++++++++++++++++
 3 files changed, 46 insertions(+), 1 deletion(-)

diff --git a/libraries/ghci/GHCi/ObjLink.hs b/libraries/ghci/GHCi/ObjLink.hs
index 1875918a921..4f63aa2f9b5 100644
--- a/libraries/ghci/GHCi/ObjLink.hs
+++ b/libraries/ghci/GHCi/ObjLink.hs
@@ -76,6 +76,9 @@ loadDLL f =
       evaluate =<< js_loadDLL (toJSString f)
       pure $ Right nullPtr
 
+-- See Note [Variable passing in JSFFI] for where
+-- __ghc_wasm_jsffi_dyld comes from
+
 foreign import javascript safe "__ghc_wasm_jsffi_dyld.loadDLL($1)"
   js_loadDLL :: JSString -> IO ()
 
diff --git a/utils/jsffi/dyld.mjs b/utils/jsffi/dyld.mjs
index c6cfedbece4..db9c8b382d6 100755
--- a/utils/jsffi/dyld.mjs
+++ b/utils/jsffi/dyld.mjs
@@ -629,7 +629,8 @@ class DyLD {
 
       // Fulfill the ghc_wasm_jsffi imports. Use new Function()
       // instead of eval() to prevent bindings in this local scope to
-      // be accessed by JSFFI code snippets.
+      // be accessed by JSFFI code snippets. See Note [Variable passing in JSFFI]
+      // for what's going on here.
       Object.assign(
         import_obj.ghc_wasm_jsffi,
         new Function(
diff --git a/utils/jsffi/post-link.mjs b/utils/jsffi/post-link.mjs
index 9e3e0643512..b5493f35817 100755
--- a/utils/jsffi/post-link.mjs
+++ b/utils/jsffi/post-link.mjs
@@ -52,6 +52,47 @@ export function parseSections(mod) {
   return recs;
 }
 
+// Note [Variable passing in JSFFI]
+// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+//
+// The JSFFI code snippets can access variables in globalThis,
+// arguments like $1, $2, etc, plus a few magic variables: __exports,
+// __ghc_wasm_jsffi_dyld and __ghc_wasm_jsffi_finalization_registry.
+// How are these variables passed to JSFFI code? Remember, we strive
+// to keep the globalThis namespace hygiene and maintain the ability
+// to have multiple Haskell-wasm apps coexisting in the same JS
+// context, so we must not pass magic variables as global variables
+// even though they may seem globally unique.
+//
+// The solution is simple: put them in the JS lambda binder position.
+// Though there are different layers of lambdas here:
+//
+// 1. User writes "$1($2, await $3)" in a JSFFI code snippet. No
+//    explicit binder here, the snippet is either an expression or
+//    some statements.
+// 2. GHC doesn't know JS syntax but it knows JS function arity from
+//    HS type signature, as well as if the JS function is async/sync
+//    from safe/unsafe annotation. So it infers the JS binder (like
+//    "async ($1, $2, $3)") and emits a (name,binder,body) tuple into
+//    the ghc_wasm_jsffi custom section.
+// 3. After link-time we collect these tuples to make a JS object
+//    mapping names to binder=>body, and this JS object will be used
+//    to fulfill the ghc_wasm_jsffi wasm imports. This JS object is
+//    returned by an outer layer of lambda which is in charge of
+//    passing magic variables.
+//
+// In case of post-linker for statically linked wasm modules,
+// __ghc_wasm_jsffi_dyld won't work so is omitted, and
+// __ghc_wasm_jsffi_finalization_registry can be created inside the
+// outer JS lambda. Only __exports is exposed as user-visible API
+// since it's up to the user to perform knot-tying by assigning the
+// instance exports back to the (initially empty) __exports object
+// passed to this lambda.
+//
+// In case of dyld, all magic variables are dyld-session-global
+// variables; dyld uses new Function() to make the outer lambda, then
+// immediately invokes it by passing the right magic variables.
+
 export async function postLink(mod) {
   let src = (
     await fs.readFile(path.join(import.meta.dirname, "prelude.mjs"), {
-- 
GitLab