▸ 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.
You are the decision synthesizer. Three probes investigated JSPI+EH feasibility, Qt-wasm JSPI support, and benefit/effort/risk. Produce a clear GO / GO-WITH-CAVEATS / NOT-NOW / BLOCKED recommendation for migrating FreeCAD-wasm from ASYNCIFY to JSPI. State the specific working (JSPI, exception-handling, Qt) combination if one exists (or exactly why none does). Give concrete migration steps, expected benefit (size/perf), effort estimate, risks, and whether the existing /opt/toolchains/qt-jspi build can be reused. Be decisive and evidence-based.
PROBE RESULTS (JSON):
[
{
"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 (-sJSPI / ASYNCIFY=2) + native wasm exceptions (-fwasm-exceptions). Both R10 blockers are resolved on the current toolchain: (1) the OCCT wasm-EH \"invalid br_table / label-arity\" miscompile does NOT reproduce with clang-22 — real OCCT sources compiled with -fwasm-exceptions produce modules that pass WebAssembly.validate, fully compile in V8, and pass binaryen wasm-opt -O2 (both legacy-EH and new try_table variants); (2) JSPI still cannot suspend across JS-EH invoke_* frames (reproduced: \"SuspendError: trying to suspend JS frames at invoke_ii\"), but -fwasm-exceptions emits NO invoke_* trampolines, so that blocker is sidestepped entirely. The remaining \"work\" is that the whole stack (OCCT + Qt + Python + FreeCAD) must be recompiled with -fwasm-exceptions instead of -fexceptions (a full rebuild, and prebuilt occt-wasm is currently JS-EH so cannot be reused), plus deployment requires a modern V8 (Chrome 137+/Node ~24+); the emsdk-bundled Node 22.16 is too old.",
"findings": [
"JSPI IS SUPPORTED in emscripten 4.0.12: settings.js has JSPI/JSPI_EXPORTS/JSPI_IMPORTS; -sJSPI maps to ASYNCIFY=2 (ASYNCIFY=2 deprecated alias). emcc warns '-sASYNCIFY=2 (JSPI) is still experimental' but it works. Basic EM_ASYNC_JS suspend under -sJSPI verified: prints 'before async / after async: 42' on Node 26 (V8 14.6).",
"DECISIVE TEST #2 (invoke_ across JS-EH) — CONFIRMS the blocker is STILL REAL: built t.cpp with -fexceptions -sJSPI where a JSPI suspend (EM_ASYNC_JS) happens inside a try/catch with a destructor (forces an invoke_ii landing-pad frame). Emscripten DOES wrap invoke_* in `new WebAssembly.Suspending`, but at runtime V8 throws `SuspendError: trying to suspend JS frames at invoke_ii (t.js:1123)`. Control build with the SAME suspend but NO landing pad (no invoke_) → suspends fine ('got 42'). So JS-EH invoke_* frames remain unsuspendable under JSPI. Independently reproduces R10's dead-end.",
"DECISIVE TEST #3 (OCCT wasm-EH miscompile) — DOES NOT REPRODUCE with clang-22. Compiled 7 real OCCT 7.8.1 sources from the R10-flagged toolkits (ChFi3d=TKFillet, ShapeFix/ShapeUpgrade=TKShHealing, BRepMesh) with `-fwasm-exceptions -fPIC`, linked into a 890 KB SIDE_MODULE with 734 functions containing the exact flagged opcodes (legacy: try/catch/catch_all/delegate/rethrow). Result on V8 14.6: WebAssembly.validate=true AND `new WebAssembly.Module` fully COMPILED OK (1145 exports). binaryen wasm-opt -O2 with EH features: exit 0. No 'br_table label arity inconsistent', no 'popping from empty stack'.",
"NEW-EH (try_table) path also now works: recompiled the same OCCT sources …
Fable
thinking
I've got three probe results in hand, so I can move forward with synthesizing the decision rather than doing more investigation. Let me structure the output using the StructuredOutput schema with fields for effort, expectedBenefit, headline, migrationSteps, and recommendation.
The evidence points clearly to GO-WITH-CAVEATS: the working combination is JSPI plus native wasm-EH with legacy encoding (emscripten's default) and a qt-jspi wasm-EH build. The main caveats are browser support limitations (Chrome-first, keeping asyncify as a fallback for Firefox and Safari), the requirement to use legacy EH encoding rather than try_table, and the need for a full dependency rebuild. Let me do a quick s …
Fable
⚙ StructuredOutput {"recommendation": "go-with-caveats", "headline": "Migrate FreeCAD-wasm from ASYNCIFY to JSPI + native wasm-EH — the R10 \"JSPI is out\" verdict is now stale; both its blockers are resolved on the current toolchain (emsdk 4.0.12 / clang-22 …