subagent transcript

session 17f09f89 · workflow wf_c636fd6b
delegated task
QUESTION: Can the FreeCAD-to-WebAssembly build migrate from ASYNCIFY to JSPI (JavaScript Promise Integration / WebAssembly stack switching), and is it
▸ Magik
QUESTION: Can the FreeCAD-to-WebAssembly build migrate from ASYNCIFY to JSPI (JavaScript Promise Integration / WebAssembly stack switching), and is it worth it? JSPI would replace ASYNCIFY's whole-module instrumentation with native stack switching — potentially a big binary-size + startup + runtime-perf win. CURRENT BUILD (verified): - Emscripten 4.0.12 at /opt/toolchains/emsdk (clang/LLVM 22, upstream/bin). Node 22 for testing. - Link options (src/Main/CMakeLists.txt ~150-250): -sASYNCIFY=1, -sASYNCIFY_STACK_SIZE=67108864, -sWASM_BIGINT=1, -sDISABLE_EXCEPTION_CATCHING=0 (i.e. **JS exceptions / -fexceptions**, NOT -fwasm-exceptions). Qt built with QT_EMSCRIPTEN_ASYNCIFY=1 (/opt/toolchains/qt-asyncify/6.11.1/wasm_singlethread). Also a post-link hack forces CPython's JS trampoline (getPyEMTrampolinePtr->0) because the wasm-gc trampoline crashes under ASYNCIFY. - WHY asyncify (from the code comment at src/Main/CMakeLists.txt:156, prior research "R10"): "ASYNCIFY + JS-EH build. This is the only combination that yields a VALID module, a rendering GUI, and working Python->OCCT. wasm-EH is out: LLVM mis-compiles OCCT's try_table/catch_all_ref to an invalid br_table (V8 rejects it). JSPI is out: JS-EH's invoke_* frames can't be suspended across; and wasm-EH is invalid. ASYNCIFY instruments the whole module so any function can suspend — Qt's synchronous exec() works." - ASYNCIFY is needed because Qt's event loop / exec() / modal dialogs / the Coin render loop run synchronously and must suspend back to the browser event loop. Any migration MUST preserve that suspend capability. THE CRUX: JSPI suspends via WebAssembly stack switching, which works cleanly across wasm frames but historically could NOT suspend across JS frames (the emscripten invoke_* exception trampolines are JS). So JSPI needs either (a) wasm exceptions (-fwasm-exceptions, no JS invoke_* frames) — but that's the OCCT br_table miscompile blocker; or (b) emscripten/V8 support for JSPI suspending across the JS-EH trampolines. INVESTIGATE (be concrete — check emscripten version support, try small test compiles in ISOLATED /tmp dirs, read the emscripten sources under /opt/toolchains/emsdk/upstream/emscripten): Paths: FreeCAD source /home/magik6k/lcad-wasm/freecad-port/FreeCAD ; toolchain env: source /home/magik6k/lcad-wasm/env.sh ; OCCT wasm /opt/toolchains/occt-wasm ; research notes may be in freecad-port/research/. Use emcc/em++/node. DO NOT touch the FreeCAD build tree at /opt/toolchains/src/freecad-gui-build (a rebuild is running there); build tests in fresh /tmp dirs only. DIMENSION A — JSPI + exception-handling core feasibility. Determine EXACTLY whether JSPI can work here now: 1. Does emscripten 4.0.12 support -sJSPI? Check the emscripten settings (/opt/toolchains/emsdk/upstream/emscripten/src/settings.js or emcc --help) for JSPI / ASYNCIFY=2 / JSPI_EXPORTS. What's the current JSPI status (stabilized in Chrome 137+, emscripten flag)? 2. THE invoke_* question: can JSPI suspend across emscripten's JS-EH invoke_* trampolines? Test it: build a tiny program that (a) uses -fexceptions (JS-EH), (b) has an EM_ASYNC_JS / a JSPI-suspending call INSIDE a try/catch or across an invoke_ frame, compile with -sJSPI, run under node --experimental-wasm-jspi (or the emsdk node 22 flag) and see if it suspends or traps. This is the decisive experiment. 3. THE wasm-EH/OCCT question: is the "LLVM miscompiles OCCT try_table/catch_all_ref to invalid br_table" still true with clang-22? Test: compile a small C++ TU that uses OCCT-style nested try/catch (or an actual small OCCT header usage from /opt/toolchains/occt-wasm/include) with -fwasm-exceptions using THIS emcc, link, and check if V8/node ACCEPTS the module (node --experimental-wasm-eh or default). If wasm-EH now works, JSPI+wasm-EH is viable. Return a clear verdict: is there a working (JSPI, EH) combination, and which. Cite the actual test outcomes.
Fable
thinking
I need to investigate emscripten's JSPI support and how it handles suspension across different exception handling mechanisms. My plan is to check the JSPI configuration in emscripten 4.0.12, test whether the invoke_* suspension works correctly with JavaScript exception handling, and then verify the same behavior with WebAssembly exception handling under clang-22, particularly with nested try/catch patterns like those in OCCT.
Fable
⚙ Bash {"command": "source /home/magik6k/lcad-wasm/env.sh 2>/dev/null; which emcc em++ node; emcc --version | head -2; node --version", "description": "Check toolchain versions"}
Fable
⚙ Bash {"command": "grep -n -i \"jspi\\|ASYNCIFY\" /opt/toolchains/emsdk/upstream/emscripten/src/settings.js | head -60", "description": "Grep JSPI/ASYNCIFY in settings.js"}
tool result
/opt/toolchains/emsdk/upstream/emscripten/emcc
/opt/toolchains/emsdk/upstream/emscripten/em++
/opt/toolchains/emsdk/node/22.16.0_64bit/bin/node
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 4.0.12 (6f625bd62ee64f4f8cd11476774741b961e4a2d5)
Copyright (C) 2025 the Emscripten authors (see AUTHORS.txt)
v22.16.0
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
tool result
813:// - 1 (default): Run binaryen's Asyncify pass to transform the code using
814://   asyncify. This emits a normal wasm file in the end, so it works everywhere,
816://   See https://emscripten.org/docs/porting/asyncify.html
817:// - 2 (deprecated): Use ``-sJSPI`` instead.
820:var ASYNCIFY = 0;
830:var ASYNCIFY_IMPORTS = [];
833:// If you know they cannot, then setting this can be extremely helpful, as otherwise asyncify
836:var ASYNCIFY_IGNORE_INDIRECT = false;
838:// The size of the asyncify  …[truncated]
Fable
thinking
JSPI is available, so I'll look into the configuration details and check what Node flags are needed for it.
Fable
⚙ Bash {"command": "sed -n '935,970p' /opt/toolchains/emsdk/upstream/emscripten/src/settings.js", "description": "Read JSPI settings block"}
tool result
// Use VM support for the JavaScript Promise Integration proposal. This allows
// async operations to happen without the overhead of modifying the wasm. This
// is experimental atm while spec discussion is ongoing, see
// https://github.com/WebAssembly/js-promise-integration/ TODO: document which
// of the following flags are still relevant in this mode (e.g. IGNORE_INDIRECT
// etc. are not needed)
//
// [link]
var JSPI = 0;

// A list of exported module functions that will be asynchronous. Each  …[truncated]
Fable
⚙ Bash {"command": "node --v8-options 2>/dev/null | grep -i \"jspi\\|stack.switch\\|wasm.*promise\\|experimental_wasm_jspi\" | head; echo \"---help wasm---\"; node --help 2>/dev/null | grep -i jspi", "description": "Check node V8 JSPI flags"}
tool result
  --trace-wasm-stack-switching (trace wasm stack switching)
        type: bool  default: --no-trace-wasm-stack-switching
  --stress-wasm-stack-switching (Always run wasm on a secondary stack, even when it is called with a regular (non-JSPI) export)
        type: bool  default: --no-stress-wasm-stack-switching
  --wasm-stack-switching-stack-size (default size of stacks for wasm stack-switching (in kB))
        type: int  default: --wasm-stack-switching-stack-size=984
  --experimental-wasm-wasmfx   …[truncated]
Fable
thinking
Node 22.16 has V8 12.4 where JSPI appears to be stabilized without needing experimental flags, so I'm going to run some decisive tests starting with a basic JSPI check, then move on to the invoke_* test and wasm exception handling with optimized compilation.

Setting up the test directories now.
Fable
⚙ Bash {"command": "node -e \"console.log(process.versions.v8)\"; echo \"---check jspi default---\"; node --v8-options 2>/dev/null | grep -i \"experimental_wasm_jspi\\|--experimental-wasm-jspi\\|wasm_jspi\"", "description": "Check V8 version and J…
tool result
14.6.202.34-node.20
---check jspi default---
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "mkdir -p /tmp/jspi-test/basic /tmp/jspi-test/invoke /tmp/jspi-test/wasmeh; echo done", "description": "Make test dirs"}
tool result
done
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
V8 14.6 is recent enough that JSPI should be stable without needing any flags, so I can write a straightforward JSPI test now.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/basic; cat > t.c <<'EOF'\n#include <emscripten.h>\n#include <stdio.h>\n\nEM_ASYNC_JS(int, do_async, (int x), {\n await new Promise(r => setTimeout(r, 10));\n return x * 2;\n});\n\nint main() {\n printf(\"be…
tool result
clang++: warning: treating 'c' input as 'c++' when in C++ mode, this behavior is deprecated [-Wdeprecated]
em++: warning: -sASYNCIFY=2 (JSPI) is still experimental [-Wexperimental]
=== RUN ===
Aborted(Assertion failed: JSPI not supported by current environment. Perhaps it needs to be enabled via flags?)
/tmp/jspi-test/basic/t.js:616
  var e = new WebAssembly.RuntimeError(what);
          ^

RuntimeError: Aborted(Assertion failed: JSPI not supported by current environment. Perhaps it needs to be   …[truncated]
Fable
thinking
I need to find the right Node.js flag for JSPI support, checking whether the environment has `WebAssembly.Suspender` or `WebAssembly.Function` with suspending capabilities.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/basic\nfor flag in \"--experimental-wasm-jspi\" \"--wasm-jspi\" \"--experimental-wasm-type-reflection\" \"--js-flags=--experimental-wasm-jspi\"; do\n echo \"=== $flag ===\"\n node $flag t.js 2>&1 | grep -i \…
tool result
=== --experimental-wasm-jspi ===
node: bad option: --experimental-wasm-jspi
=== --wasm-jspi ===
node: bad option: --wasm-jspi
=== --experimental-wasm-type-reflection ===
before async
after async: 42
=== --js-flags=--experimental-wasm-jspi ===
node: bad option: --js-flags=--experimental-wasm-jspi
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
So the `--experimental-wasm-type-reflection` flag actually enabled JSPI support, even though `--experimental-wasm-jspi` itself isn't recognized as a direct Node option. It seems JSPI might be stable in V8 14.6 by default, but emscripten's tests specifically check for type reflection support to verify it's working.

Now I'm setting up the real test: can JSPI suspend through the invoke_ trampolines that emscripten uses for exception handling? When `-fexceptions` is enabled, calls that might throw get wrapped in JS try/catch frames via `invoke_vii` and similar functions. Historically JSPI couldn't suspend across JS frames at all, so this will be the critical test — building a program that suspe  …
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/invoke; source /home/magik6k/lcad-wasm/env.sh 2>/dev/null\ncat > t.cpp <<'EOF'\n#include <emscripten.h>\n#include <stdio.h>\n#include <stdexcept>\n\nEM_ASYNC_JS(int, do_async, (int x), {\n await new Promise(r…
tool result
em++: warning: -sASYNCIFY=2 (JSPI) is still experimental [-Wexperimental]
=== check invoke_ imports in wasm ===
invoke_
invoke_ii
invoke_iii
invoke_v
invoke_vi
invoke_vii
=== RUN ===
/tmp/jspi-test/invoke/t.js:966
              imports[x] = original = new WebAssembly.Suspending(original);
                                      ^

TypeError: WebAssembly.Suspending is not a constructor
    at Object.instrumentWasmImports (/tmp/jspi-test/invoke/t.js:966:39)
    at getWasmImports (/tmp/jspi-test/invo  …[truncated]
Fable
thinking
I'm seeing that the WebAssembly.Suspending constructor error only appears with the type-reflection flag, while the actual JSPI stack-switching API requires a different flag. The basic test worked earlier and printed the expected output using EM_ASYNC_JS with the same instrumentWasmImports approach, so I'm trying to figure out why that succeeded but this one's failing.

The key difference might be the optimization level — the basic build used -O0 while the invoke test used -O1, though both should theoretically use Suspending. I'm wondering if Node.js needs the `--experimental-wasm-jspi` flag or if there's a specific V8 flag I'm missing. Since V8 14.6 has JSPI as stable, it should be available  …
Fable
⚙ Bash {"command": "node --v8-options 2>/dev/null | grep -iB1 -A1 \"jspi\\|suspend\\|promise.integration\\|stack.switching\" | head -40", "description": "Search all V8 options for jspi/suspend"}
tool result
        type: bool  default: --no-trace-wasm-streaming
  --trace-wasm-stack-switching (trace wasm stack switching)
        type: bool  default: --no-trace-wasm-stack-switching
  --stress-wasm-stack-switching (Always run wasm on a secondary stack, even when it is called with a regular (non-JSPI) export)
        type: bool  default: --no-stress-wasm-stack-switching
  --wasm-stack-switching-stack-size (default size of stacks for wasm stack-switching (in kB))
        type: int  default: --wasm-stack  …[truncated]
Fable
thinking
JSPI appears to be stable in V8 14.6 since the experimental flag is gone, which should make WebAssembly.Suspending available by default. But it's not showing up unless type-reflection is enabled — the basic test worked with that flag, so I need to rerun it and compare to see what's actually different between the two setups. understand what the detection logic is doing in the generated code.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test; node --experimental-wasm-type-reflection -e \"console.log('Suspending:', typeof WebAssembly.Suspending); console.log('Function:', typeof WebAssembly.Function); console.log('promising:', typeof WebAssembly.pro…
tool result
Suspending: function
Function: function
promising: function
=== no flag ===
Suspending: function
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
So `WebAssembly.Suspending` exists as a function, but it's not actually a constructor—it doesn't have the [[Construct]] internal slot. The error makes sense now; emscripten's runtime might be polyfilling or checking for this differently. I should test whether calling `new` on it directly works or if there's something else going on with how the module is handling it.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test\necho \"=== type-reflection ===\"; node --experimental-wasm-type-reflection -e \"try{ new WebAssembly.Suspending(()=>{}); console.log('construct OK') }catch(e){ console.log('ERR', e.message) }\"\necho \"=== no…
tool result
=== type-reflection ===
construct OK
=== no flag ===
construct OK
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
The `new WebAssembly.Suspending` call works at the top level, but it's failing inside the invoke test module—that's puzzling. I'm wondering if emscripten's generated code is doing some feature detection on `WebAssembly.Suspending` earlier, or maybe the minified runtime in the -O1 build is renaming the `WebAssembly` variable somehow. Let me check what's actually happening at line 966 in the generated invoke t.js file.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/invoke; sed -n '955,975p' t.js", "description": "Inspect instrumentWasmImports"}
tool result
      runtimeKeepaliveCounter -= 1;
    };
  var Asyncify = {
  instrumentWasmImports(imports) {
        var importPattern = /^(invoke_.*|__asyncjs__.*)$/;
  
        for (let [x, original] of Object.entries(imports)) {
          if (typeof original == 'function') {
            let isAsyncifyImport = original.isAsync || importPattern.test(x);
            // Wrap async imports with a suspending WebAssembly function.
            if (isAsyncifyImport) {
              imports[x] = original = new Web  …[truncated]
Fable
thinking
I'm realizing the critical mechanism here: when JSPI and `-fexceptions` are both enabled, emscripten's `Asyncify.instrumentWasmImports` automatically wraps every invoke_ trampoline function as a suspending import using `WebAssembly.Suspending`, which is what allows the async/await pattern to work across the wasm boundary.

But there's a puzzling contradiction — the error says `WebAssembly.Suspending is not a constructor`, yet I can verify it IS a constructor at the top level. The issue might be that inside the module context, something's different — maybe a stale compiled artifact, or the `WebAssembly` object itself is shadowed somehow. Let me re-run the test fresh to see what's actually hap  …
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/invoke; echo \"=== with type-reflection ===\"; node --experimental-wasm-type-reflection t.js 2>&1 | tail -8; echo \"=== no flag ===\"; node t.js 2>&1 | grep -i \"before\\|after\\|got\\|caught\\|guard\\|Error\\…
tool result
=== with type-reflection ===
          ^

RuntimeError: Aborted(LinkError: WebAssembly.instantiate(): Import #0 "env" "__linear_memory": memory import must be a WebAssembly.Memory object). Build with -sASSERTIONS for more info.
    at abort (/tmp/jspi-test/invoke/t.js:368:11)
    at instantiateArrayBuffer (/tmp/jspi-test/invoke/t.js:416:5)
    at async createWasm (/tmp/jspi-test/invoke/t.js:513:16)

Node.js v26.2.0
=== no flag ===
failed to asynchronously prepare wasm: LinkError: WebAssembly.ins  …[truncated]
Fable
thinking
I'm noticing the Node version shifted from v22.16.0 to v26.2.0 — looks like the system is picking up a different Node installation now instead of the emsdk one. I should explicitly use the emsdk node to keep things consistent.

The real issue though: I accidentally compiled with the `-c` flag which created an object file and overwrote the linked WebAssembly binary, causing the LinkError. I need to rebuild without that flag and stick with the emsdk node at `/opt/toolchains/emsdk/node/22.16.0_64bit/bin/node` since that's what the task specifies for testing.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/invoke; source /home/magik6k/lcad-wasm/env.sh 2>/dev/null\nNODE=/opt/toolchains/emsdk/node/22.16.0_64bit/bin/node\nrm -f t.js t.wasm\nem++ -O1 -fexceptions -sJSPI=1 -sWASM_BIGINT=1 -sDISABLE_EXCEPTION_CATCHING…
tool result
=== RUN (emsdk node 22, type-reflection) ===
                                      ^

TypeError: WebAssembly.Suspending is not a constructor
    at Object.instrumentWasmImports (/tmp/jspi-test/invoke/t.js:966:39)
    at getWasmImports (/tmp/jspi-test/invoke/t.js:451:12)
    at createWasm (/tmp/jspi-test/invoke/t.js:496:14)
    at Object.<anonymous> (/tmp/jspi-test/invoke/t.js:1107:1)
    at Module._compile (node:internal/modules/cjs/loader:1730:14)
    at Object..js (node:internal/modules/cjs/lo  …[truncated]
Fable
thinking
I'm noticing a discrepancy with the emsdk node version and WebAssembly.Suspending support. The basic test seemed to work, but I need to clarify which node version actually ran it and whether emsdk node 22.16 is the issue here.

Let me directly test whether WebAssembly.Suspending works as a constructor on emsdk node 22.16 to resolve this contradiction.
Fable
⚙ Bash {"command": "NODE=/opt/toolchains/emsdk/node/22.16.0_64bit/bin/node\necho \"emsdk node version:\"; $NODE --version\necho \"=== type-reflection ===\"; $NODE --experimental-wasm-type-reflection -e \"console.log('Suspending typeof:', typeof We…
tool result
emsdk node version:
v22.16.0
=== type-reflection ===
Suspending typeof: undefined
ctor ERR: WebAssembly.Suspending is not a constructor
=== --experimental-wasm-jspi ===
1
=== list jspi-ish v8 opts on emsdk node ===
  --trace-wasm-stack-switching (trace wasm stack switching)
        type: bool  default: --no-trace-wasm-stack-switching
  --wasm-stack-switching-stack-size (default size of stacks for wasm stack-switching (in kB))
        type: int  default: --wasm-stack-switching-stack-size=984
  --  …[truncated]
Fable
thinking
On older Node versions like 22.16 with V8 ~12.x, I need the `--experimental-wasm-jspi` and `--experimental-wasm-stack-switching` flags to get WebAssembly.Suspending working, whereas type-reflection alone isn't enough. The earlier test that seemed to work with just type-reflection must have been running on a different Node version where JSPI was already enabled by default.

Let me test the invoke case with the correct flags on emsdk node 22.16, and also run tests on the newer system node v26.2.0 which has stable JSPI support — that'll better match what Chrome 137+ offers.
Fable
⚙ Bash {"command": "NODE=/opt/toolchains/emsdk/node/22.16.0_64bit/bin/node\ncd /tmp/jspi-test/invoke\necho \"=== emsdk node22 --experimental-wasm-jspi ===\"\n$NODE --experimental-wasm-jspi t.js 2>&1 | tail -20", "description": "Run invoke test wit…
tool result
=== emsdk node22 --experimental-wasm-jspi ===
/tmp/jspi-test/invoke/t.js:966
              imports[x] = original = new WebAssembly.Suspending(original);
                                      ^

TypeError: WebAssembly.Suspending is not a constructor
    at Object.instrumentWasmImports (/tmp/jspi-test/invoke/t.js:966:39)
    at getWasmImports (/tmp/jspi-test/invoke/t.js:451:12)
    at createWasm (/tmp/jspi-test/invoke/t.js:496:14)
    at Object.<anonymous> (/tmp/jspi-test/invoke/t.js:1107:1)
    a  …[truncated]
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/basic\necho \"=== basic on emsdk node22 --experimental-wasm-jspi ===\"\n/opt/toolchains/emsdk/node/22.16.0_64bit/bin/node --experimental-wasm-jspi t.js 2>&1 | tail -5\necho \"=== basic on system node26 (defaul…
tool result
=== basic on emsdk node22 --experimental-wasm-jspi ===
    at Object..js (node:internal/modules/cjs/loader:1895:10)
    at Module.load (node:internal/modules/cjs/loader:1465:32)
    at Function._load (node:internal/modules/cjs/loader:1282:12)

Node.js v22.16.0
=== basic on system node26 (default, no flag) ===
before async
after async: 42
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
So the issue is that emsdk's bundled Node 22.16 has an older V8 version that still uses the `WebAssembly.Suspender`/`promising` API, but emscripten 4.0.12 expects the newer `WebAssembly.Suspending` constructor syntax—they're incompatible, which is why the JSPI support isn't working even with the experimental flag enabled.

System Node v26.2.0 with V8 14.6 has stable JSPI support where `WebAssembly.Suspending` works out of the box, and that aligns with Chrome 137+ which is the actual target. Now I need to run the full invoke test on Node 26.2.0 to see if JSPI works correctly with exceptions and suspend calls across try/catch boundaries.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/invoke\necho \"=== node version ===\"; node --version\necho \"=== INVOKE+JSPI test on system node26 ===\"\nnode t.js 2>&1 | tail -20", "description": "Decisive invoke_ JSPI test on node26"}
tool result
=== node version ===
v26.2.0
=== INVOKE+JSPI test on system node26 ===
before
wasm://wasm/00018bca:1


SuspendError: trying to suspend JS frames
    at wasm://wasm/00018bca:wasm-function[17]:0x496
    at invoke_ii (/tmp/jspi-test/invoke/t.js:1123:36)
    at wasm://wasm/00018bca:wasm-function[18]:0x55c
    at wasm://wasm/00018bca:wasm-function[19]:0x644
    at wrapper (/tmp/jspi-test/invoke/t.js:973:20)
    at callMain (/tmp/jspi-test/invoke/t.js:1177:15)
    at doRun (/tmp/jspi-test/invoke/t.js:  …[truncated]
Fable
thinking
So the core issue is that even with emscripten 4.0.12 wrapping the `invoke_ii` function in `WebAssembly.Suspending`, V8 still throws a `SuspendError` when the suspension actually crosses a JS frame boundary at that point. The wrapping didn't solve the problem because the suspension is happening within the JS frame itself rather than at a safe boundary. allows suspending across a JS frame that isn't itself a Suspending boundary returning a promise. So the result is a SuspendError.

This confirms the R10 blocker is still present in emscripten 4.0.12 with node V8 14.6 — JSPI can't suspend through the JS-EH invoke_* trampolines. The Suspending wrapper doesn't solve it because the suspension orig  …
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/invoke; source /home/magik6k/lcad-wasm/env.sh 2>/dev/null\n# Control: same suspend but NO try/catch => no invoke_ frame around the suspending call\ncat > noinvoke.cpp <<'EOF'\n#include <emscripten.h>\n#include…
tool result
=== control (fexceptions build but suspend NOT under a landing pad) ===
              imports[x] = original = new WebAssembly.Suspending(original);
TypeError: WebAssembly.Suspending is not a constructor
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
The issue is that emsdk's node 22 is being prepended to PATH after sourcing env.sh, which is causing the WebAssembly.Suspending constructor error. I need to use the system node explicitly by finding its full path or using /usr/bin/node directly to avoid this conflict.
Fable
⚙ Bash {"command": "command -v node; ls -la $(command -v node) 2>/dev/null; echo \"---\"; for p in /usr/bin/node /usr/local/bin/node; do [ -x $p ] && echo \"$p -> $($p --version)\"; done", "description": "Locate system node 26"}
tool result
Exit code 1
/bin/node
-rwxr-xr-x 1 nobody nobody 59893432 May 23 15:53 /bin/node
---
/usr/bin/node -> v26.2.0
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "N26=/usr/bin/node\ncd /tmp/jspi-test/invoke\necho \"=== control (no invoke_ frame) on node26 ===\"\n$N26 noinvoke.js 2>&1 | grep -i \"before\\|got\\|Suspend\\|Error\" | head\necho \"=== reconfirm invoke test on node26 ===\"\n$N…
tool result
=== control (no invoke_ frame) on node26 ===
before
got 42
=== reconfirm invoke test on node26 ===
before
SuspendError: trying to suspend JS frames
    at invoke_ii (/tmp/jspi-test/invoke/t.js:1123:36)
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
I've confirmed the key distinction: when suspension happens outside of a try/catch landing pad, everything works fine and returns the expected result, but when suspension occurs within a callee that's invoked through an exception handling trampoline, it fails with a suspension error. This confirms that the R10 blocker is a genuine issue.

The real problem for FreeCAD is that JSPI can't suspend across emscripten's invoke_* frames, which are everywhere in a -fexceptions build—they wrap any code inside try blocks or with C++ destructors. Since Qt's event loop relies on suspension and OCCT/Qt code is full of exception handling, this combination is fundamentally broken. The only viable path forwa  …
Fable
⚙ Bash {"command": "ls /opt/toolchains/occt-wasm/ 2>/dev/null; echo \"---include---\"; ls /opt/toolchains/occt-wasm/include 2>/dev/null | head; echo \"---libs---\"; ls /opt/toolchains/occt-wasm/lib 2>/dev/null | head", "description": "Inspect occt…
tool result
bin
include
lib
share
---include---
opencascade
---libs---
cmake
libTKBin.a
libTKBinL.a
libTKBinTObj.a
libTKBinXCAF.a
libTKBO.a
libTKBool.a
libTKBRep.a
libTKCAF.a
libTKCDF.a
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
I need to figure out how OCCT was compiled and whether it used the right exception handling flags. The core issue is testing whether I can compile WebAssembly code with wasm-EH that works with OCCT's patterns, and whether V8 will accept it. Ideally I'd recompile OCCT with -fwasm-exceptions to properly test the mismatch, but that's a massive undertaking since the libraries were prebuilt with JavaScript exception handling to match FreeCAD.

The real claim I'm investigating is that LLVM is generating invalid br_table instructions in OCCT's try/catch patterns when using -fwasm-exceptions, which V8 then rejects. Let me try reproducing this with clang-22 by writing representative C++ code with nes  …
Fable
⚙ Bash {"command": "find /opt/toolchains/occt-wasm/include -name \"Standard_Failure.hxx\" -o -name \"Standard_ErrorHandler.hxx\" 2>/dev/null | head\necho \"--- how was OCCT built? check a lib for wasm-eh tags ---\"\ncd /tmp/jspi-test/wasmeh\nar x …
tool result
/opt/toolchains/occt-wasm/include/opencascade/Standard_ErrorHandler.hxx
/opt/toolchains/occt-wasm/include/opencascade/Standard_Failure.hxx
--- how was OCCT built? check a lib for wasm-eh tags ---
obj: FSD_Base64.cxx.o
--- look for tag/exception section indicating EH model ---
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh\nOD=/opt/toolchains/emsdk/upstream/bin/wasm-objdump\n# find an object that has exception handling\nfor f in $(ls *.o | head -40); do\n imports=$($OD -x \"$f\" 2>/dev/null | grep -c \"invoke_\")\n tags…
tool result
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
I'm shifting my approach to look for WebAssembly exception handling opcodes directly—either `try_table` from the newer exception handling spec or `invoke_` imports that indicate exception support. Let me scan through libTKernel to see what exception mechanisms are actually being used.

Actually, the most direct way is to just write a test case that reproduces the miscompilation using OCCT's own exception handling patterns, specifically their Standard_ErrorHandler macros and try-catch blocks.
Fable
⚙ Bash {"command": "grep -n \"define OCC_CATCH_SIGNALS\\|try\\b\\|catch\" /opt/toolchains/occt-wasm/include/opencascade/Standard_ErrorHandler.hxx | head; echo \"=== macro ===\"; grep -rn \"OCC_CATCH_SIGNALS\" /opt/toolchains/occt-wasm/include/open…
tool result
36://! that can be used in the code (often inside try {} blocks) to convert C-style 
52:  #define OCC_CATCH_SIGNALS   Standard_ErrorHandler _aHandler; \
66:  #define OCC_CATCH_SIGNALS
87:  //! Create a ErrorHandler (to be used with try{}catch(){}).
116:  //! Test if the code is currently running in a try block
124:  //! Warning: If no catch is prepared for this exception, it displays the
145:  //! the try {} catch {} block from being bypassed by OCC signal or exception.
151:  //! that of the try  …[truncated]
Fable
thinking
Now I'm setting up a comprehensive test for wasm exception handling codegen—I'll create C++ code that reproduces the nested try/catch pattern with rethrow and catch_all_ref that generates br_table, then compile it with -fwasm-exceptions and verify it runs correctly in V8 through Node 26. I'm also planning to compile an actual OCCT header-using translation unit to make sure the test is realistic.

Writing test code...
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh; source /home/magik6k/lcad-wasm/env.sh 2>/dev/null\ncat > weh.cpp <<'EOF'\n#include <cstdio>\n#include <stdexcept>\n\nstruct Dtor { int id; ~Dtor(){ printf(\"~Dtor %d\\n\", id); } };\n\n__attribute__((n…
tool result
=== compile with -fwasm-exceptions ===
exit: 0
-rwxr-xr-x 1 magik6k magik6k 22947 Jul  6 01:09 weh.wasm
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh\necho \"=== validate module with wasm-validate ===\"\n/opt/toolchains/emsdk/upstream/bin/wasm-validate --enable-exceptions weh.wasm 2>&1; echo \"validate exit: $?\"\necho \"=== run on emsdk node22 ===\"…
tool result
=== validate module with wasm-validate ===
(eval):3: no such file or directory: /opt/toolchains/emsdk/upstream/bin/wasm-validate
validate exit: 127
=== run on emsdk node22 ===
/opt/toolchains/emsdk/node/22.16.0_64bit/bin/node: bad option: --experimental-wasm-eh
=== run on system node26 ===
~Dtor 1
~Dtor 0
--- x=0 -> 0
inner catch-all
~Dtor 1
outer runtime: one
~Dtor 0
--- x=1 -> 10
inner catch-all
~Dtor 1
outer catch-all
~Dtor 0
--- x=2 -> 30
inner logic: three
~Dtor 1
outer exception: three
~Dt  …[truncated]
Fable
thinking
The wasm-EH test with complex nested exception handling is working correctly across all test cases on node26—destructors run properly when no exception is thrown, runtime errors get caught and rethrown through multiple levels, integer throws are handled by catch-all blocks, and logic errors follow the expected catch path.

Now I need to test with actual OCCT code to reproduce the specific br_table miscompilation that was flagged. Since node22 doesn't support wasm-EH, I'll need to use node26 or Chrome137+. The challenge is that prebuilt OCCT libraries with JS-EH don't link easily, but I can compile a real OCCT source file with -fwasm-exceptions and validate the generated module with binaryen   …
Fable
⚙ Bash {"command": "ls /opt/toolchains/src/ 2>/dev/null | head -30; echo \"=== find OCCT source dirs ===\"; find /opt/toolchains -maxdepth 3 -type d -iname \"*occt*\" 2>/dev/null | head; find / -maxdepth 5 -type d -iname \"occt*\" 2>/dev/null | gr…
tool result
boost-1.86.0
fmt-11.1.4
freecad-gui-build
ftbase.c.o
googletest-1.15.2
icu
OCCT-7_8_1
pyside-host-gen-build
Python-3.14.4
xerces-c-3.2.5
yaml-cpp-0.8.0
=== find OCCT source dirs ===
/opt/toolchains/occt-wasm
/opt/toolchains/src/OCCT-7_8_1
/opt/toolchains/occt-wasm
/opt/toolchains/src/OCCT-7_8_1
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "cd /opt/toolchains/src/OCCT-7_8_1\n# Find source files that use OCC_CATCH_SIGNALS heavily (the flagged pattern)\ngrep -rl \"OCC_CATCH_SIGNALS\" src/ 2>/dev/null | head -20\necho \"=== count ===\"\ngrep -rl \"OCC_CATCH_SIGNALS\"…
tool result
src/AIS/AIS_TexturedShape.cxx
src/AIS/AIS_Shape.cxx
src/BOPAlgo/BOPAlgo_PaveFiller_7.cxx
src/BOPAlgo/BOPAlgo_Tools.cxx
src/BOPAlgo/BOPAlgo_PaveFiller.cxx
src/BOPAlgo/BOPAlgo_ArgumentAnalyzer.cxx
src/BOPAlgo/BOPAlgo_CheckerSI.cxx
src/BOPAlgo/BOPAlgo_PaveFiller_5.cxx
src/BOPAlgo/BOPAlgo_Builder.cxx
src/BOPAlgo/BOPAlgo_PaveFiller_6.cxx
src/BOPAlgo/BOPAlgo_PaveFiller_2.cxx
src/BOPAlgo/BOPAlgo_PaveFiller_4.cxx
src/BOPAlgo/BOPAlgo_RemoveFeatures.cxx
src/BOPAlgo/BOPAlgo_PaveFiller_3.cxx
src/BRepMesh/BR  …[truncated]
Fable
thinking
I'm going to compile several representative OCCT source files with the `-fwasm-exceptions` flag to object files, then use LLVM tools to disassemble and validate them—this should expose the invalid `br_table` instruction that manifests during validation.

Actually, the cleanest approach is to compile a real OCCT source file with `em++ -fwasm-exceptions -c` to an object, then disassemble it with `llvm-objdump` to look for the problematic `br_table` inside the exception handling pad. To truly verify the bug, I need a runnable module that V8 will reject during instantiation, so I should compile a real OCCT source file with its minimal dependencies into a standalone module with stubs. But that's   …
Fable
⚙ Bash {"command": "ls /home/magik6k/lcad-wasm/freecad-port/research/ 2>/dev/null; echo \"===\"; grep -rli \"br_table\\|wasm-exceptions\\|wasm-eh\\|try_table\\|catch_all_ref\\|R10\\|JSPI\" /home/magik6k/lcad-wasm/freecad-port/research/ 2>/dev/null…
tool result
R10-eh-jspi-asyncify-findings.md
R1-codebase-architecture.md
R2-dependency-matrix.md
R3-graphics-coin-webgl.md
R4-python-runtime.md
R5-dialogs-files-jspi.md
R6-payload-memory-threads.md
R7-wasm-threads-decision.md
R8-jspi-migration-plan.md
R9-coin-webgl-strategy.md
===
/home/magik6k/lcad-wasm/freecad-port/research/R4-python-runtime.md
/home/magik6k/lcad-wasm/freecad-port/research/R9-coin-webgl-strategy.md
/home/magik6k/lcad-wasm/freecad-port/research/R2-dependency-matrix.md
/home/magik6k/lcad-wa  …[truncated]
Fable
⚙ Bash {"command": "grep -n \"br_table\\|try_table\\|catch_all_ref\\|miscompile\\|invalid\\|reproduc\\|\\.cxx\\|OCCT\" /home/magik6k/lcad-wasm/freecad-port/research/R10-eh-jspi-asyncify-findings.md | head -40", "description": "Read R10 br_table de…
tool result
5:OCCT's exception-heavy geometry kernel coexist in one wasm module. Every viable
25:3. **OCCT wasm exceptions.** OCCT is exception-pervasive (`Standard_Failure`
33:| **native wasm-EH (new, try_table)** | JSPI | **INVALID wasm.** LLVM mis-compiles OCCT's `try_table`/`catch_all_ref`+rethrow into a `br_table` with inconsistent label arity. V8: *"br_table: label arity inconsistent"*; Binaryen: *"popping from empty stack"*. `llvm-objdump` reads it but it is not valid wasm. Only OCCT (TKFillet/TKGeom  …[truncated]
Fable
thinking
I've pinpointed the exact reproducer: the miscompile happens with OCCT libraries (TKFillet, TKGeomAlgo, TKShHealing, TKTopAlgo) and manifests as "br_table: label arity inconsistent" in V8 or "popping from empty stack" in Binaryen. The issue stems from `try_table`/`catch_all_ref` with rethrow having inconsistent label arity, and `-Os` doesn't sidestep it. R10 already tested JS-EH plus JSPI and found the same underlying problem.

Now I need to verify whether this wasm-EH miscompile still exists with clang-22 (LLVM 22), since R10's testing was done at an earlier LLVM version. The approach is to compile the actual offending OCCT source files from those toolkits with `-fwasm-exceptions` using cla  …
Fable
⚙ Bash {"command": "cd /opt/toolchains/src/OCCT-7_8_1\n# Which offending toolkit sources use rethrow inside catch\necho \"=== files in named toolkits with 'throw;' rethrow ===\"\nfor pkg in ChFi3d ChFi2d Blend BRepFill GeomAlgo BRepMesh Geom2dInt …
tool result
=== files in named toolkits with 'throw;' rethrow ===
=== total files with bare rethrow in src ===
3
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
Looking at the pattern, it seems the bare rethrows are mostly coming from compiler-generated cleanup code during exception unwinding rather than explicit throw statements. The real trigger appears to be OCC_CATCH_SIGNALS style try blocks where objects need cleanup. Instead of trying to trace each case individually, I should just compile a broader set of examples to see the pattern more clearly.

I'll compile OCCT sources from the problematic toolkits using -fwasm-exceptions and run wasm-opt on them to catch the "popping from empty stack" error directly. This will let me test the miscompile with clang-22 in a more systematic way. I need to set up the include paths properly—the flattened heade  …
Fable
⚙ Bash {"command": "cd /opt/toolchains/src/OCCT-7_8_1\nINC=/opt/toolchains/occt-wasm/include/opencascade\nls $INC/*.hxx | wc -l\n# find a couple of source files in the offending toolkits\nfind src/ChFi3d src/ShapeFix src/ShapeUpgrade src/BRepMesh …
tool result
7434
src/ChFi3d/ChFi3d.cxx
src/ChFi3d/ChFi3d_Builder.cxx
src/ChFi3d/ChFi3d_Builder_0.cxx
src/ChFi3d/ChFi3d_Builder_1.cxx
src/ChFi3d/ChFi3d_Builder_2.cxx
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh; source /home/magik6k/lcad-wasm/env.sh 2>/dev/null\nINC=/opt/toolchains/occt-wasm/include/opencascade\nSRC=/opt/toolchains/src/OCCT-7_8_1\n# try compiling one heavy OCCT file with wasm-EH to object\nem+…
tool result
compile exit: 0
-rw-r--r-- 1 magik6k magik6k 316323 Jul  6 01:11 ChFi3d_Builder_0.o
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
The object compiles, so now I need to validate it and check for the miscompile. The binaryen error about popping from an empty stack occurs when wasm-opt processes it, so let me try running wasm-opt directly on the object file—though I'm not sure if it can handle relocatable objects without the `-g` flag. The real test would be to either compile the source to a standalone module or link it into an executable, but that depends on resolving dependencies first.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh; source /home/magik6k/lcad-wasm/env.sh 2>/dev/null\nWO=/opt/toolchains/emsdk/upstream/bin/wasm-opt\necho \"=== wasm-opt on object (enable EH) ===\"\n$WO --enable-exception-handling --enable-bulk-memory …
tool result
=== wasm-opt on object (enable EH) ===
warning: linking section is present, so this is not a standard wasm file - binaryen cannot handle this properly!
warning: feature  was enabled by the user, but disallowed in the features section.warning: no passes specified, not doing any work
exit: 0
=== also try llvm-objdump to inspect try_table opcodes ===
9
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
I'm seeing that the relocatable object has multiple exception handling references that wasm-opt can't fully validate since it's not a standard module yet. The real test would be building a small executable with the actual OCCT codegen from those toolkits, linking it against the OCCT libraries, and validating through V8—but since the OCCT libs were compiled with JS-EH, I'd need to compile those specific source files myself with the right flags to avoid a mismatch.

A better approach: link the compiled objects into a proper wasm module using wasm-ld with undefined symbols allowed and no entry point, which produces a real (non-relocatable) module I can validate with V8. The validation happens a  …
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh; source /home/magik6k/lcad-wasm/env.sh 2>/dev/null\nINC=/opt/toolchains/occt-wasm/include/opencascade\nSRC=/opt/toolchains/src/OCCT-7_8_1\n# compile a batch of offending-toolkit sources\nFILES=\"src/ChF…
tool result
FAIL compile src/ChFi3d/ChFi3d_Builder_0.cxx src/ChFi3d/ChFi3d_Builder_1.cxx src/ChFi3d/ChFi3d_Builder_2.cxx src/ShapeFix/ShapeFix_Shape.cxx src/ShapeFix/ShapeFix_Wire.cxx src/ShapeUpgrade/ShapeUpgrade_UnifySameDomain.cxx src/BRepMesh/BRepMesh_Delaun.cxx
clang++: error: no such file or directory: '/opt/toolchains/src/OCCT-7_8_1/src/ChFi3d/ChFi3d_Builder_0.cxx src/ChFi3d/ChFi3d_Builder_1.cxx src/ChFi3d/ChFi3d_Builder_2.cxx src/ShapeFix/ShapeFix_Shape.cxx src/ShapeFix/ShapeFix_Wire.cxx src/ShapeUp  …[truncated]
Fable
thinking
The issue is that zsh doesn't automatically word-split unquoted variables like bash does, so I need to switch to bash or use an array instead to properly iterate over the file list.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh; rm -f *.o\nbash -c '\nsource /home/magik6k/lcad-wasm/env.sh 2>/dev/null\nINC=/opt/toolchains/occt-wasm/include/opencascade\nSRC=/opt/toolchains/src/OCCT-7_8_1\nFILES=\"src/ChFi3d/ChFi3d_Builder_0.cxx s…
tool result
compiled 7/7
7
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
Now I'm linking all seven compiled modules into a single non-relocatable WebAssembly module with undefined symbols allowed, then validating it against V8 in Node 26 to confirm the exception handling implementation is correct with clang-22.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh\nbash -c '\nsource /home/magik6k/lcad-wasm/env.sh 2>/dev/null\n# Link the OCCT wasm-EH objects into a real module, allow undefined -> imports\nem++ -O2 -fwasm-exceptions -sERROR_ON_UNDEFINED_SYMBOLS=0 -…
tool result
link exit: 0
-rwxr-xr-x 1 magik6k magik6k 116 Jul  6 01:12 occt_eh.wasm
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
The linker is stripping out all the code because nothing's being exported or referenced. I need to preserve the OCCT functions by either exporting all symbols or disabling garbage collection during linking, so I'll relink with flags to keep everything and make it visible.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh\nbash -c '\nsource /home/magik6k/lcad-wasm/env.sh 2>/dev/null\nem++ -O2 -fwasm-exceptions -sERROR_ON_UNDEFINED_SYMBOLS=0 -sWASM_BIGINT=1 \\\n -sSTANDALONE_WASM -Wl,--no-entry -Wl,--no-gc-sections -Wl,-…
tool result
link exit: 1
wasm-ld: error: ChFi3d_Builder_0.o: undefined symbol: vtable for GeomAdaptor_Surface
wasm-ld: error: ChFi3d_Builder_0.o: undefined symbol: typeinfo for ChFiDS_Spine
wasm-ld: error: ChFi3d_Builder_0.o: undefined symbol: typeinfo for ChFiDS_FilSpine
wasm-ld: error: ChFi3d_Builder_0.o: undefined symbol: typeinfo for Geom2d_Curve
wasm-ld: error: too many errors emitted, stopping now (use -error-limit=0 to see all errors)
em++: error: '/opt/toolchains/emsdk/upstream/bin/wasm-ld -o occt_e  …[truncated]
Fable
thinking
I'm seeing a critical issue with the emscripten configuration: `--export-all` combined with `--no-gc-sections` forces all undefined symbols to resolve, which means vtables and typeinfo must be available, so `--allow-undefined` won't work in this setup. More importantly, the linker is using both legacy and modern exception handling flags simultaneously—`-mllvm -wasm-use-legacy-eh` alongside `-mllvm -wasm-enable-eh` and `-lc++-legacyexcept`—which suggests emscripten 4.0.12 is defaulting to legacy wasm exception handling.

The real puzzle is whether the compiled object files actually use the legacy exnref opcodes or the newer try_table/catch_all_ref standard, and whether either produces a valid  …
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh\n# What EH opcodes are actually in my compiled object? Distinguish legacy (catch/try) vs standard (try_table/catch_all_ref)\nOD=/opt/toolchains/emsdk/upstream/bin/llvm-objdump\necho \"=== opcode histogr…
tool result
=== opcode histogram in ChFi3d_Builder_0.o ===
   2242 try
   1674 catch_all
    943 delegate
    567 rethrow
    174 catch
      9 br_table
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
The object is using the legacy WebAssembly exception handling opcodes rather than the newer try_table approach, which means emcc 4.0.12 defaults to the legacy EH proposal—though Binaryen has trouble parsing it and V8's validity is uncertain. The br_table instructions are just standard switch statements.

Now I need to test whether V8 in Node 26 can actually accept a real module with these legacy-EH functions. I'm thinking about how to link everything together: my seven object files plus the OCCT libraries, with a minimal main that references enough symbols to keep the functions from being garbage collected. The challenge is that without --export-all, the linker's gc-sections will strip unref  …
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh\ncat > main.cpp <<'EOF'\n#include <cstdio>\nint main(){ printf(\"occt-eh module loaded\\n\"); return 0; }\nEOF\nbash -c '\nsource /home/magik6k/lcad-wasm/env.sh 2>/dev/null\nLIB=/opt/toolchains/occt-was…
tool result
Exit code 2
link exit: 1
wasm-ld: error: /opt/toolchains/occt-wasm/lib/libTKBool.a(TopOpeBRepDS_BuildTool.cxx.o): undefined symbol: __resumeException
wasm-ld: error: /opt/toolchains/occt-wasm/lib/libTKBool.a(TopOpeBRepDS_BuildTool.cxx.o): undefined symbol: __resumeException
wasm-ld: error: /opt/toolchains/occt-wasm/lib/libTKBool.a(TopOpeBRepDS_BuildTool.cxx.o): undefined symbol: __resumeException
wasm-ld: error: /opt/toolchains/occt-wasm/lib/libTKBool.a(TopOpeBRepDS_BuildTool.cxx.o): undefined s  …[truncated]
Fable
thinking
The prebuilt OCCT libraries were compiled with JavaScript exception handling enabled, but my linker is using the legacy WebAssembly exception runtime, which doesn't include the `__resumeException` symbol that the prebuilt libs expect—this is a fundamental mismatch in exception handling implementations.

Rather than rebuild OCCT entirely with WebAssembly exceptions (which would be massive), I can validate the code section structurally by creating a side module from my compiled objects. This would let V8 validate the exception handling opcodes and control flow without needing all the external symbols to resolve, giving me a definitive answer about whether the codegen is actually valid.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh\nbash -c '\nsource /home/magik6k/lcad-wasm/env.sh 2>/dev/null\n# Build a SIDE_MODULE from the wasm-EH OCCT objects -> undefined refs become imports, valid standalone wasm\nem++ -O2 -fwasm-exceptions -sS…
tool result
link exit: 0
-rwxr-xr-x 1 magik6k magik6k 91 Jul  6 01:13 occt_eh_side.wasm
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
The dead code elimination is stripping everything because side modules only keep exported symbols by default. I need to preserve all functions, so I'll try using `-sLINKABLE=1` to export everything and disable dead code elimination. Though this might bring back the vtable and typeinfo undefined issues, so I may need to adjust the approach.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh\nbash -c '\nsource /home/magik6k/lcad-wasm/env.sh 2>/dev/null\nem++ -O2 -fwasm-exceptions -sSIDE_MODULE=1 -sLINKABLE=1 -sWASM_BIGINT=1 \\\n ChFi3d_Builder_0.o ChFi3d_Builder_1.o ChFi3d_Builder_2.o Shap…
tool result
link exit: 0
-rwxr-xr-x 1 magik6k magik6k 91 Jul  6 01:14 occt_eh_side.wasm
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
I'm wondering if the issue is that these are LTO bitcode objects, which might be getting treated differently during the link step—the optimizer could be dropping unreferenced functions even though they have external linkage, unless LINKABLE is explicitly set to preserve them.

Let me verify what's actually happening: earlier llvm-objdump showed wasm opcodes, so these are definitely wasm objects, not bitcode. That means the DCE at link time is removing them because nothing references them and they're not exported. For a side module, I should try combining --no-gc-sections with --export-all to force keeping all symbols, though I'm puzzled why LINKABLE didn't already achieve this since it's sup  …
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh\nfile ChFi3d_Builder_0.o\necho \"=== head bytes ===\"; xxd ChFi3d_Builder_0.o | head -1\necho \"=== full side.log ===\"; cat side.log\necho \"=== 91-byte module contents ===\"; /opt/toolchains/emsdk/ups…
tool result
ChFi3d_Builder_0.o: WebAssembly (wasm) binary version 0x1 (MVP module)
=== head bytes ===
00000000: 0061 736d 0100 0000 01a8 8680 8000 5a60  .asm..........Z`
=== full side.log ===
=== 91-byte module contents ===

occt_eh_side.wasm:	file format wasm

Disassembly of section CODE:

00000000 <CODE>:
        # 1 functions in section.

00000001 <__wasm_call_ctors>:

       3: 0b           	end
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
The issue is that the side module's weak symbols are being stripped by the linker's dead code elimination. I need to disable garbage collection for the side module with `--no-gc-sections` and use `--export-all` to ensure the OCCT class methods are properly exported instead of being discarded.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh\nbash -c '\nsource /home/magik6k/lcad-wasm/env.sh 2>/dev/null\nem++ -O2 -fwasm-exceptions -sSIDE_MODULE=1 -sWASM_BIGINT=1 \\\n -Wl,--no-gc-sections -Wl,--export-all \\\n ChFi3d_Builder_0.o ChFi3d_Buil…
tool result
link exit: 1
wasm-ld: error: ChFi3d_Builder_0.o: relocation R_WASM_MEMORY_ADDR_SLEB cannot be used against symbol `.L.str.14`; recompile with -fPIC
wasm-ld: error: ChFi3d_Builder_0.o: relocation R_WASM_MEMORY_ADDR_SLEB cannot be used against symbol `.L.str.14`; recompile with -fPIC
wasm-ld: error: ChFi3d_Builder_0.o: relocation R_WASM_MEMORY_ADDR_SLEB cannot be used against symbol `typeinfo for Standard_TypeMismatch`; recompile with -fPIC
wasm-ld: error: ChFi3d_Builder_0.o: relocation R_WASM_TAB  …[truncated]
Fable
thinking
I need to recompile those 7 object files with the -fPIC flag alongside -fwasm-exceptions to make them compatible with the side module approach. The -fPIC flag shouldn't significantly alter the exception handling codegen, though there's a nagging concern that it might change the generated code enough that it won't accurately represent how the actual static FreeCAD build behaves.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh; rm -f *.o\nbash -c '\nsource /home/magik6k/lcad-wasm/env.sh 2>/dev/null\nINC=/opt/toolchains/occt-wasm/include/opencascade\nSRC=/opt/toolchains/src/OCCT-7_8_1\nFILES=\"src/ChFi3d/ChFi3d_Builder_0.cxx s…
tool result
compiled 7/7 with -fPIC
side link exit: 0
-rwxr-xr-x 1 magik6k magik6k 890010 Jul  6 01:15 occt_eh_side.wasm
func count:
734
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
I've got a substantial 890KB module with 734 functions using OCCT's legacy WebAssembly exception handling codegen, so now I need to validate it against V8 in Node 26 to see if it actually works. I'll run WebAssembly.validate to check the module structure, then attempt instantiation and compilation to trigger full function-body validation. Since the module uses legacy EH opcodes, I need to verify whether V8 14.6 has the legacy EH proposal enabled by default or if it requires a flag.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh\ncat > val.js <<'EOF'\nconst fs = require('fs');\nconst buf = fs.readFileSync('occt_eh_side.wasm');\nconsole.log('bytes:', buf.length);\nconsole.log('WebAssembly.validate:', WebAssembly.validate(buf));\…
tool result
=== node26 default ===
bytes: 890010
WebAssembly.validate: true
WebAssembly.Module: COMPILED OK, exports: 1145
=== node26 legacy-eh flags ===
bytes: 890010
WebAssembly.validate: true
WebAssembly.Module: COMPILED OK, exports: 1145
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
The OCCT WebAssembly module with exception handling is validating and compiling successfully in V8 14.6, which means the 734 functions from the real OCCT geometry libraries are working properly with the legacy exception handling opcodes.

The key insight is that emcc 4.0.12 now defaults to legacy wasm-EH rather than the new try_table approach that R10 flagged as problematic, and the legacy path is validating cleanly in V8 even though R10 said Binaryen couldn't parse it. I've tested 7 files across three of the four flagged toolkits so far, but I should also check TKGeomAlgo and TKTopAlgo, plus test whether the new try_table path specifically still breaks as R10 reported. There's also the ques  …
Fable
⚙ Bash {"command": "grep -n \"WASM_LEGACY_EXCEPTIONS\\|LEGACY_EXCEPTION\\|use-legacy-eh\\|wasm_legacy\" /opt/toolchains/emsdk/upstream/emscripten/src/settings.js | head\necho \"=== run binaryen wasm-opt on the legacy-EH module (full validate) ===\…
tool result
786:var WASM_LEGACY_EXCEPTIONS = true;
=== run binaryen wasm-opt on the legacy-EH module (full validate) ===
wasm-opt(legacy) exit: 0
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
Binaryen successfully processes the legacy exception handling version of the OCCT module with the `-legacy` flags, which tells it to handle the older EH opcodes. Now I'm switching to test the new exception handling model using try_table by setting `WASM_LEGACY_EXCEPTIONS=0` — this is where the R10 failure occurs. I need to recompile with this setting and then validate it through both the validator and Binaryen to see where the issue emerges.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh; mkdir -p new_eh; \nbash -c '\nsource /home/magik6k/lcad-wasm/env.sh 2>/dev/null\nINC=/opt/toolchains/occt-wasm/include/opencascade\nSRC=/opt/toolchains/src/OCCT-7_8_1\n# include TKGeomAlgo (GeomAPI/BRe…
tool result
compiled(new-eh) 4/4
=== opcodes in new_eh object ===
   2242 try_table
   1483 catch_all_ref
    759 catch_all
    671 throw_ref
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
I'm working with the new exception handling standard that uses try_table, catch_all_ref, and throw_ref—this matches the R10 invalid br_table case. I should create a test module and verify it works correctly with V8 and binaryen.
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh\nbash -c '\nsource /home/magik6k/lcad-wasm/env.sh 2>/dev/null\nem++ -O2 -fwasm-exceptions -sWASM_LEGACY_EXCEPTIONS=0 -sSIDE_MODULE=1 -sWASM_BIGINT=1 \\\n -Wl,--no-gc-sections -Wl,--export-all \\\n new…
tool result
link exit: 0
-rwxr-xr-x 1 magik6k magik6k 761096 Jul  6 01:16 occt_neweh_side.wasm
func count:
603
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/wasmeh\ncat > valnew.js <<'EOF'\nconst fs = require('fs');\nconst buf = fs.readFileSync('occt_neweh_side.wasm');\nconsole.log('bytes:', buf.length, 'validate:', WebAssembly.validate(buf));\ntry { const m = new…
tool result
=== V8 node26 (new try_table EH) ===
bytes: 761096 validate: true
Module COMPILED OK, exports: 972
=== binaryen wasm-opt on new-EH module ===
[wasm-validator error in function 310] unexpected false: Block type requires additional features, on 
(block $block23 (result i32 exnref)
 (try_table (catch_ref $eimport$0 $block23)
  (drop
   (call $fimport$140
    (i32.add
     (local.get $9)
     (i32.const 8)
    )
   )
wasm-opt(new) exit: 141
zshexit:1: parameter HISTDB_FD does not contain a file desc  …[truncated]
Fable
thinking
The new try_table path is showing really interesting results—V8 accepts it and compiles the module successfully with nearly a thousand exports, which means the clang-22 exception handling codegen isn't being rejected like it was before. But Binaryen's hitting a wall with a block type error that requires additional features for the exnref result type in the try_table construct.

The issue is that I didn't pass the right feature flags to Binaryen—it needs the correct exception handling flag to understand the new exnref model, not just the legacy try/catch support. What's crucial here is that my emscripten build with WASM_LEGACY_EXCEPTIONS=0 and -O2 actually succeeded at the link stage, which m  …
Fable
⚙ Bash {"command": "/opt/toolchains/emsdk/upstream/bin/wasm-opt --help 2>&1 | grep -iE \"exception|exnref|reference-types|typed-continu|all-features|mvp\" | head\necho \"=== retry wasm-opt on new-EH module with --all-features ===\"\ncd /tmp/jspi-t…
tool result
                                                --emit-exnref 
  --emit-exnref                                 After running all requested 
                                                memory.fill to wasm mvp and 
                                                operations to wasm mvp and 
                                                wasm mvp and disable the sign 
  --translate-to-exnref                         translate old Phase 3 EH 
                                                exnref  …[truncated]
Fable
thinking
Running binaryen with all features enabled shows that version 123 handles the new try_table and exnref exception handling cleanly, which means the earlier compilation failure was due to incorrect feature flags rather than a real incompatibility. So for this dimension, the R10 miscompile issue doesn't actually reproduce with the current toolchain setup.

Both the legacy and new exception handling approaches compile real OCCT source files successfully, validate as proper WebAssembly modules, and pass through V8 and binaryen optimization without issues—confirming that the LLVM codegen bug blocking wasm-EH has been fixed. This opens up the possibility of using wasm-EH with JSPI, since native was  …
Fable
⚙ Bash {"command": "cd /tmp/jspi-test/invoke\nbash -c '\nsource /home/magik6k/lcad-wasm/env.sh 2>/dev/null\n# JSPI + wasm-EH: suspend inside try/catch with dtor cleanup (NO invoke_ frames now)\nem++ -O1 -fwasm-exceptions -sJSPI=1 -sWASM_BIGINT=1 t…
tool result
=== invoke_ imports present? ===
invoke_
(none above = good; wasm-EH removes invoke_ trampolines)
=== RUN JSPI+wasm-EH on node26 ===
before
got 42
guard dtor
after
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
I'm seeing the key breakthrough: when using JSPI with wasm exceptions and the `-fwasm-exceptions -sJSPI` flags, the suspend point lands inside a try/catch block with a Guard destructor—the exact scenario that previously failed under JS-EH with that "SuspendError" about trying to suspend JS frames. But with wasm-EH, those problematic invoke trampolines disappear entirely, which resolves the issue.

The test output confirms it works: the suspension cleanly crosses the try/catch frame, resumes correctly, the destructor runs, and there's no trap. So the solution is JSPI paired with native wasm exceptions, which eliminates both blockers from the earlier documentation—the OCCT miscompile is fixed,  …
Fable
I have decisive results for all three sub-questions. Let me compile the findings.
Fable
⚙ StructuredOutput {"dimension": "A — JSPI + exception-handling core feasibility (emscripten 4.0.12 / clang-LLVM 22 / binaryen v123 / V8 14.6)", "verdict": "feasible-with-work. A working (JSPI, EH) combination now EXISTS and was proven end-to-end: JSPI (-sJSP…
tool result
Structured output provided successfully