Roll forward stateful loop_emscripten changes. (#11801)

This is a roll forward of https://github.com/iree-org/iree/pull/11507

Successful CI run:
https://github.com/iree-org/iree/actions/runs/3897710708/jobs/6655737094

---

I had been testing locally with
[`experimental/web/testing/build_tests.sh`](https://github.com/iree-org/iree/blob/main/experimental/web/testing/build_tests.sh),
which sets `-DCMAKE_BUILD_TYPE=RelWithDebInfo`. The CI uses
[`build_tools/cmake/build_runtime_emscripten.sh`](https://github.com/iree-org/iree/blob/main/build_tools/cmake/build_runtime_emscripten.sh),
which leaves the build type unset and defaults to `Release`.

Release builds were failing with `SyntaxError: Unexpected token
(1:72626)` in `/emsdk/upstream/emscripten/tools/acorn-optimizer.js:1852`
with this code:

![image](https://user-images.githubusercontent.com/4010439/211941096-c2041567-08e7-44bf-b770-ed64b4e76b60.png)

While `for (const [key, value] of Object.entries(object1))` is the
recommended way to iterate through an object's enumerable string-keyed
property key-value pairs
([source](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries)),
Emscripten does not appear to support that newer syntax out of the box.
diff --git a/build_tools/cmake/iree_copts.cmake b/build_tools/cmake/iree_copts.cmake
index 5f3740d..f0612cb 100644
--- a/build_tools/cmake/iree_copts.cmake
+++ b/build_tools/cmake/iree_copts.cmake
@@ -358,6 +358,16 @@
     "-natvis:${IREE_ROOT_DIR}/runtime/iree.natvis"
 )
 
+# Our Emscripten library code uses dynCall, which needs these link flags.
+# TODO(scotttodd): Find a way to refactor this, this is nasty to always set :(
+if(EMSCRIPTEN)
+  iree_select_compiler_opts(IREE_DEFAULT_LINKOPTS
+    ALL
+      "-sDYNCALLS=1"
+      "-sEXPORTED_RUNTIME_METHODS=['dynCall']"
+  )
+endif()
+
 #-------------------------------------------------------------------------------
 # Size-optimized build flags
 #-------------------------------------------------------------------------------
diff --git a/experimental/web/testing/build_tests.sh b/experimental/web/testing/build_tests.sh
index 2cf46ba..b83d740 100644
--- a/experimental/web/testing/build_tests.sh
+++ b/experimental/web/testing/build_tests.sh
@@ -59,6 +59,7 @@
     -DIREE_HAL_EXECUTABLE_LOADER_VMVX_MODULE=ON \
     -DIREE_BUILD_SAMPLES=OFF \
     -DIREE_ENABLE_CPUINFO=OFF \
+    -DIREE_ENABLE_ASAN=OFF \
     -DIREE_BUILD_TESTS=ON
 
 echo "=== Building default targets ==="
diff --git a/runtime/src/iree/base/loop_emscripten.c b/runtime/src/iree/base/loop_emscripten.c
index 8b4bd50..983e04a 100644
--- a/runtime/src/iree/base/loop_emscripten.c
+++ b/runtime/src/iree/base/loop_emscripten.c
@@ -16,8 +16,14 @@
 // externs from loop_emscripten.js
 //===----------------------------------------------------------------------===//
 
-extern iree_status_t loop_command_call(iree_loop_callback_fn_t callback,
-                                       void* user_data, iree_loop_t loop);
+typedef uint32_t iree_loop_emscripten_scope_t;  // Opaque handle.
+
+extern iree_loop_emscripten_scope_t iree_loop_allocate_scope();
+extern void iree_loop_free_scope(iree_loop_emscripten_scope_t scope);
+
+extern iree_status_t iree_loop_command_call(iree_loop_emscripten_scope_t scope,
+                                            iree_loop_callback_fn_t callback,
+                                            void* user_data, iree_loop_t loop);
 
 //===----------------------------------------------------------------------===//
 // iree_loop_emscripten_t
@@ -25,9 +31,7 @@
 
 typedef struct iree_loop_emscripten_t {
   iree_allocator_t allocator;
-
-  // TODO(scotttodd): handle to a "scope"/object (managed in JS), so multiple
-  //                  loops can exist at once
+  iree_loop_emscripten_scope_t scope;
 } iree_loop_emscripten_t;
 
 IREE_API_EXPORT iree_status_t iree_loop_emscripten_allocate(
@@ -37,6 +41,7 @@
   IREE_RETURN_IF_ERROR(
       iree_allocator_malloc(allocator, sizeof(*loop), (void**)&loop));
   loop->allocator = allocator;
+  loop->scope = iree_loop_allocate_scope();
   *out_loop = loop;
   return iree_ok_status();
 }
@@ -45,9 +50,7 @@
   IREE_ASSERT_ARGUMENT(loop);
   iree_allocator_t allocator = loop->allocator;
 
-  // TODO(scotttodd): cleanup:
-  //     abort pending operations (neuter callbacks/Promises)
-  //     assert if any work is still outstanding
+  iree_loop_free_scope(loop->scope);
 
   // After all operations are cleared we can release the data structures.
   iree_allocator_free(allocator, loop);
@@ -56,8 +59,8 @@
 static iree_status_t iree_loop_emscripten_run_call(
     iree_loop_emscripten_t* loop_emscripten, iree_loop_call_params_t* params) {
   iree_loop_t loop = iree_loop_emscripten(loop_emscripten);
-  return loop_command_call(params->callback.fn, params->callback.user_data,
-                           loop);
+  return iree_loop_command_call(loop_emscripten->scope, params->callback.fn,
+                                params->callback.user_data, loop);
 }
 
 // Control function for the Emscripten loop.
diff --git a/runtime/src/iree/base/loop_emscripten.h b/runtime/src/iree/base/loop_emscripten.h
index 4f8c6b7..4156b77 100644
--- a/runtime/src/iree/base/loop_emscripten.h
+++ b/runtime/src/iree/base/loop_emscripten.h
@@ -35,7 +35,6 @@
                          const void* params, void** inout_ptr);
 
 // Returns a loop that uses |data|.
-// TODO(scotttodd): rework structs with "scope" so 2+ loops can exist at once
 static inline iree_loop_t iree_loop_emscripten(iree_loop_emscripten_t* data) {
   iree_loop_t loop = {
       data,
diff --git a/runtime/src/iree/base/loop_emscripten.js b/runtime/src/iree/base/loop_emscripten.js
index 06190d3..2baf8f1 100644
--- a/runtime/src/iree/base/loop_emscripten.js
+++ b/runtime/src/iree/base/loop_emscripten.js
@@ -11,33 +11,113 @@
 //   * https://github.com/evanw/emscripten-library-generator
 //   * https://github.com/emscripten-core/emscripten/tree/main/src
 
-const LibraryLoopEmscripten = {
-  $loop_emscripten_support__postset: 'loop_emscripten_support();',
-  $loop_emscripten_support: function() {
-    class LoopEmscripten {
-      constructor() {
-        // TODO(scotttodd): store state here
+const IreeLibraryLoopEmscripten = {
+  $iree_loop_emscripten_support__postset: 'iree_loop_emscripten_support();',
+  $iree_loop_emscripten_support: function() {
+    const IREE_STATUS_OK = 0;
+    const IREE_STATUS_CODE_MASK = 0x1F;
+    const IREE_STATUS_ABORTED = 10 & IREE_STATUS_CODE_MASK;
+    const IREE_STATUS_OUT_OF_RANGE = 11 & IREE_STATUS_CODE_MASK;
+
+    class LoopCommand {
+      abort() {}
+    }
+
+    // IREE_LOOP_COMMAND_CALL
+    class LoopCommandCall extends LoopCommand {
+      constructor(scope, operationId, callback, user_data, loop) {
+        super();
+
+        this.callback = callback;
+        this.user_data = user_data;
+        this.loop = loop;
+
+        this.timeoutId = setTimeout(() => {
+          Module['dynCall'](
+              'iiii', this.callback, this.user_data, this.loop, IREE_STATUS_OK);
+          // TODO(scotttodd): handle the returned status (sticky failure state?)
+          //     at least free the status so it doesn't leak
+          delete scope.pendingOperations[operationId];
+        }, 0);
       }
 
-      loop_command_call(callback, user_data, loop) {
-        const IREE_STATUS_OK = 0;
+      abort() {
+        clearTimeout(this.timeoutId);
+        Module['dynCall'](
+            'iiii', this.callback, this.user_data, this.loop,
+            IREE_STATUS_ABORTED);
+      }
+    }
 
-        setTimeout(() => {
-          const ret =
-              Module['dynCall_iiii'](callback, user_data, loop, IREE_STATUS_OK);
-          // TODO(scotttodd): handle the returned status (sticky failure state?)
-        }, 0);
+    class LoopEmscriptenScope {
+      constructor() {
+        this.nextOperationId = 0;
 
+        // Dictionary of operationIds -> LoopCommands.
+        this.pendingOperations = {};
+      }
+
+      destroy() {
+        for (const id in this.pendingOperations) {
+          const operation = this.pendingOperations[id];
+          operation.abort();
+          delete this.pendingOperations[id];
+        }
+      }
+
+      command_call(callback, user_data, loop) {
+        // TODO(scotttodd): assert not destroyed to avoid reentrant queueing?
+        const operationId = this.nextOperationId++;
+        this.pendingOperations[operationId] =
+            new LoopCommandCall(this, operationId, callback, user_data, loop);
         return IREE_STATUS_OK;
       }
     }
 
-    const instance = new LoopEmscripten();
-    _loop_command_call = instance.loop_command_call.bind(instance);
-  },
+    class LoopEmscripten {
+      constructor() {
+        this.nextScopeHandle = 0;
 
-  loop_command_call: function() {},
-  loop_command_call__deps: ['$loop_emscripten_support'],
+        // Dictionary of scopeHandles -> LoopEmscriptenScopes.
+        this.scopes = {};
+      }
+
+      iree_loop_allocate_scope() {
+        const scopeHandle = this.nextScopeHandle++;
+        this.scopes[scopeHandle] = new LoopEmscriptenScope();
+        return scopeHandle;
+      }
+
+      iree_loop_free_scope(scope_handle) {
+        if (!(scope_handle in this.scopes)) return;
+
+        const scope = this.scopes[scope_handle];
+        scope.destroy();
+        delete this.scopes[scope_handle];
+      }
+
+      iree_loop_command_call(scope_handle, callback, user_data, loop) {
+        if (!(scope_handle in this.scopes)) return IREE_STATUS_OUT_OF_RANGE;
+
+        const scope = this.scopes[scope_handle];
+        return scope.command_call(callback, user_data, loop);
+      }
+    }
+
+    const instance = new LoopEmscripten();
+    _iree_loop_allocate_scope =
+        instance.iree_loop_allocate_scope.bind(instance);
+    _iree_loop_free_scope = instance.iree_loop_free_scope.bind(instance);
+    _iree_loop_command_call = instance.iree_loop_command_call.bind(instance);
+  },
+  $iree_loop_emscripten_support__deps: ['$dynCall'],
+
+  iree_loop_allocate_scope: function() {},
+  iree_loop_allocate_scope__deps: ['$iree_loop_emscripten_support'],
+  iree_loop_free_scope: function() {},
+  iree_loop_free_scope__deps: ['$iree_loop_emscripten_support'],
+  iree_loop_command_call: function() {},
+  iree_loop_command_call__deps: ['$iree_loop_emscripten_support'],
 }
 
-mergeInto(LibraryManager.library, LibraryLoopEmscripten);
+mergeInto(LibraryManager.library, IreeLibraryLoopEmscripten);