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 B — Qt-for-wasm JSPI support + what must be rebuilt. Investigate: 1. Does Qt 6.11 support a JSPI mode? Qt-for-wasm has QT_EMSCRIPTEN_ASYNCIFY; is there a JSPI equivalent (Qt docs / configure flags / QTBUG)? Check the Qt wasm platform plugin sources if available under /opt/toolchains/qt* or qtsrc. Qt historically used asyncify for its synchronous event loop; does newer Qt use JSPI? 2. Would the existing asyncify-built Qt (/opt/toolchains/qt-asyncify) work under a JSPI FreeCAD link, or must Qt be REBUILT for JSPI? (Likely rebuilt — the asyncify instrumentation is baked into Qt's static libs.) Note there's also /opt/toolchains/qt-jspi (78M) in the toolchain — investigate whether a JSPI Qt build ALREADY EXISTS there and its state (was it an earlier attempt?). Check rebuild-qt-jspi*.sh scripts in /opt/toolchains. 3. What FreeCAD-side link/flag changes are needed (ASYNCIFY removed, JSPI added, the CPython trampoline hack, EXPORTED_FUNCTIONS that must become JSPI-suspending). Return the concrete list of what must change/rebuild for a JSPI FreeCAD, and whether a JSPI Qt already exists to reuse.
Fable
thinking
I'm looking into what needs to be rebuilt for Qt-for-wasm to support JSPI, starting with checking the toolchain directories.
Fable
⚙ Bash {"command": "ls -la /opt/toolchains/ | head -50; echo \"---QT DIRS---\"; ls -d /opt/toolchains/qt* 2>/dev/null; echo \"---JSPI QT---\"; ls -la /opt/toolchains/qt-jspi 2>/dev/null; echo \"---REBUILD SCRIPTS---\"; ls -la /opt/toolchains/rebui…
tool result
total 20112
drwx------ 1 magik6k magik6k      758 Jul  5 12:43 .
drwxr-xr-x 1 nobody  nobody       224 Jul  5 17:46 ..
drwx------ 1 magik6k magik6k       20 Jul  4 06:22 boost-wasm
drwx------ 1 magik6k magik6k      844 Jul  4 04:42 emsdk
-rw------- 1 magik6k magik6k     1128 Jul  4 03:04 fix-freetype-sjlj.sh
drwx------ 1 magik6k magik6k       20 Jul  4 06:22 fmt-wasm
drwx------ 1 magik6k magik6k      870 Jul  4 00:57 freecad  …[truncated]
Fable
thinking
I see there's a JSPI Qt available, so let me check its current state and look at the rebuild scripts.
Fable
⚙ Bash {"command": "echo \"=== qt-jspi tree ===\"; find /opt/toolchains/qt-jspi -maxdepth 3 -type d | head -40; echo; echo \"=== qt-jspi size/libs ===\"; du -sh /opt/toolchains/qt-jspi; ls -la /opt/toolchains/qt-jspi/6.11.1/wasm_singlethread/lib/*…
tool result
=== qt-jspi tree ===
/opt/toolchains/qt-jspi
/opt/toolchains/qt-jspi/6.11.1
/opt/toolchains/qt-jspi/6.11.1/wasm_singlethread
/opt/toolchains/qt-jspi/6.11.1/wasm_singlethread/lib
/opt/toolchains/qt-jspi/6.11.1/wasm_singlethread/mkspecs
/opt/toolchains/qt-jspi/6.11.1/wasm_singlethread/bin
/opt/toolchains/qt-jspi/6.11.1/wasm_singlethread/libexec
/opt/toolchains/qt-jspi/6.11.1/wasm_singlethread/share
/opt/toolchains/qt-jspi/6.11.1/wasm_singlethread/doc
/opt/toolchains/qt-jspi/6.11.1/wasm_singlethrea  …[truncated]
Fable
thinking
I found a JSPI Qt build at /opt/toolchains/qt-jspi that's about 76MB, so I'm going to check the rebuild scripts to see how it was constructed and what condition it's in.
Fable
⚙ Bash {"command": "echo \"=== rebuild-qt-jspi.sh ===\"; cat /opt/toolchains/rebuild-qt-jspi.sh; echo; echo \"=== rebuild-qt-jspi-jseh.sh ===\"; cat /opt/toolchains/rebuild-qt-jspi-jseh.sh", "description": "Read JSPI Qt rebuild scripts"}
tool result
=== rebuild-qt-jspi.sh ===
#!/usr/bin/env bash
set -e
TC=/opt/toolchains
source $TC/emsdk/emsdk_env.sh 2>/dev/null
QTHOST=$TC/qt/6.11.1/gcc_64
PREFIX=$TC/qt-jspi/6.11.1/wasm_singlethread
rm -rf $TC/qt-jspi $TC/qtsrc
mkdir -p $TC/qtsrc && cd $TC/qtsrc

echo "=== download ==="
for m in qtbase qtsvg qttools; do
  curl -sL "https://download.qt.io/official_releases/qt/6.11/6.11.1/submodules/${m}-everywhere-src-6.11.1.tar.xz" -o $m.tar.xz
  tar xJf $m.tar.xz && rm $m.tar.xz && mv ${m}-everywhere-src-6  …[truncated]
Fable
thinking
So there were three different build attempts with varying exception handling strategies: the first used Qt's default setup combining JSPI with wasm-exceptions, the second patched Qt's gate to allow JSPI paired with JS-EH instead, and the third was exploring JSPI with wasm-exceptions but got cut off.

Qt 6.11 does support JSPI through the `-feature-wasm-jspi` flag, though Qt's configure gate normally requires wasm-exceptions alongside it—the jseh variant worked around this by removing that constraint. Now I need to figure out which build actually produced the current `/opt/toolchains/qt-jspi` by comparing timestamps and checking what exception handling was compiled in, then review the wasmeh   …
Fable
⚙ Bash {"command": "echo \"=== rebuild-qt-jspi-wasmeh.sh ===\"; cat /opt/toolchains/rebuild-qt-jspi-wasmeh.sh", "description": "Read wasmeh Qt rebuild script"}
tool result
=== rebuild-qt-jspi-wasmeh.sh ===
#!/usr/bin/env bash
# Rebuild Qt with JSPI + wasm-EH (-fexceptions). Uses the already-downloaded,
# gate-patched qtsrc. JS-EH avoids the Binaryen parse bug on OCCT's wasm-EH
# try_table output while JSPI still fixes the CPython-trampoline conflict.
set -e
TC=/opt/toolchains
source $TC/emsdk/emsdk_env.sh 2>/dev/null
QTHOST=$TC/qt/6.11.1/gcc_64
PREFIX=$TC/qt-jspi/6.11.1/wasm_singlethread
rm -rf $TC/qt-jspi $TC/qtsrc/qtbase-build $TC/qtsrc/qtsvg-build $TC/qtsrc/qtt  …[truncated]
Fable
thinking
I need to figure out which build variant created the current qt-jspi binary from July 4th at 05:44, and the wasmeh script from a minute earlier might not have run successfully. Let me check the actual compiled Qt libraries to see whether they're using wasm exception handling with try_table instructions or JavaScript exception handling with invoke patterns, and look for JSPI markers in the qtwasm platform plugin source and Qt configuration.
Fable
⚙ Bash {"command": "cd /opt/toolchains/qt-jspi/6.11.1/wasm_singlethread\necho \"=== qconfig / features ===\"; grep -rl \"wasm_jspi\\|wasm-jspi\\|JSPI\\|jspi\" mkspecs/ include/QtCore/*.h 2>/dev/null | head\necho \"--- qconfig.pri features ---\"; g…
tool result
=== qconfig / features ===
mkspecs/qmodule.pri
mkspecs/qconfig.pri
mkspecs/modules/qt_lib_core.pri
mkspecs/modules/qt_lib_core_private.pri
mkspecs/common/wasm/wasm.conf
mkspecs/features/wasm/wasm.prf
include/QtCore/qconfig.h
--- qconfig.pri features ---
mkspecs/qmodule.pri:QT.global_private.enabled_features = gc_binaries reduce_exports localtime_r posix_fallocate system-zlib gui network printsupport sql testlib widgets xml dlopen trivial_auto_var_init_pattern libcpp_hardening cross_compile large  …[truncated]
Fable
thinking
The current qt-jspi build has both wasm-exceptions and wasm-jspi enabled, meaning it was compiled with the wasmeh script combining JSPI and wasm-EH support. That's the key issue—while wasm-exceptions in Qt itself is fine, linking Qt with wasm-EH enabled together with OCCT (also wasm-EH) into a single binary creates a problem.

The real constraint is that JSPI requires either wasm-exceptions everywhere (which blocks OCCT's br_table optimization) or JSPI needs to suspend across JS-EH invoke frames. The existing qt-jspi uses wasm-exceptions, but there's also a jseh variant script that patches Qt's gate to allow JSPI with JS-EH instead. The installed version is wasmeh with both features enabled,  …
Fable
⚙ Bash {"command": "cd /opt/toolchains/qtsrc/qtbase 2>/dev/null || echo \"NO QTSRC qtbase\"\necho \"=== qtsrc present? ===\"; ls /opt/toolchains/qtsrc/\necho\necho \"=== JSPI / asyncify refs in wasm platform plugin ===\"\ngrep -rin \"jspi\\|QT_EMS…
tool result
=== qtsrc present? ===
qtbase
qtbase-build
qtsvg
qtsvg-build
qttools
qttools-build

=== JSPI / asyncify refs in wasm platform plugin ===
src/corelib/platform/wasm/qstdweb.cpp:97:EM_JS(bool, jsHaveAsyncify, (), { return typeof Asyncify !== "undefined"; });
src/corelib/platform/wasm/qstdweb.cpp:98:EM_JS(bool, jsHaveJspi, (),
src/corelib/platform/wasm/qstdweb.cpp:99:      { return typeof Asyncify !== "undefined" && !!Asynci  …[truncated]
Fable
thinking
Qt 6.11's wasm platform plugin has built-in JSPI support that detects availability at runtime and manages suspend/resume control accordingly. I need to examine the suspend mechanism in the control implementation and understand how Qt leverages the async JavaScript integration mechanism.

Writing async JS integration...
Fable
⚙ Bash {"command": "cd /opt/toolchains/qtsrc/qtbase/src/corelib/platform/wasm\necho \"=== qwasmsuspendresumecontrol.cpp lines 40-160 ===\"; sed -n '40,160p' qwasmsuspendresumecontrol.cpp\necho; echo \"=== qstdweb_p.h around 30-60 (wasm_jspi) ===\"…
tool result
=== qwasmsuspendresumecontrol.cpp lines 40-160 ===
    control to the browser's event loop (without suspending), and will call the C++
    event handlers directly in that case.
*/

Q_GLOBAL_STATIC(QWasmSuspendResumeControl, s_suspendResumeControl);

// Setup/constructor function for Module.suspendResumeControl.
// FIXME if assigning to the Module object from C++ is/becomes possible
// then this does not need to be a separate JS function.
void qtSuspendResumeControlClearJs() {
    EM_ASM({
        …[truncated]
Fable
thinking
So Qt 6.11 uses `EM_ASYNC_JS` to create Promise-based suspend points that work transparently with both asyncify and JSPI — the runtime detects which mechanism is available and uses the appropriate one. This is a significant shift because it means the same suspension code path handles both approaches seamlessly, rather than requiring separate implementations.

The real constraint emerges when considering the call stack: JSPI can't suspend across JS-EH invoke_ trampolines. If FreeCAD is built with JS exceptions and Python calls into OCCT which then triggers Qt's suspend, those invoke_ frames would block the suspension. The current qt-jspi build uses wasm-exceptions throughout to avoid this, bu  …
Fable
⚙ Bash {"command": "cd /opt/toolchains\necho \"=== plugins present in qt-jspi (platform plugin, imageformats) ===\"\nls qt-jspi/6.11.1/wasm_singlethread/plugins/platforms/ 2>/dev/null\necho \"--- total libs count jspi vs asyncify ---\"\necho \"jsp…
tool result
=== plugins present in qt-jspi (platform plugin, imageformats) ===
libqwasm.a
libqwasm.prl
objects-Release
qtloader.js
qtlogo.svg
wasm_shell.html
--- total libs count jspi vs asyncify ---
jspi: 19 libs
async: 19 libs
--- diff lib lists ---
SAME LIB SET

=== EH style check: disassemble libQt6Core for try_table (wasm-EH) vs invoke/__cxa (JS-EH) ===
(eval):16: no matches found: *.o
obj: archdetect.cpp.o
try_table count: 0
JS-EH sym refs: 44
zshexit:1: parameter HISTDB_FD does not co  …[truncated]
Fable
thinking
The first object I checked doesn't have exception handling, so that approach won't work. I need to look at a file that actually uses exceptions, or search the entire archive for exception markers like `__cpp_exceptions` and `eh` to get a clearer picture of what's enabled.
Fable
⚙ Bash {"command": "cd /tmp/qtjspi-eh && rm -f *.o\n/opt/toolchains/emsdk/upstream/bin/llvm-ar x /opt/toolchains/qt-jspi/6.11.1/wasm_singlethread/lib/libQt6Core.a\n# Check target-features section on a real object for exception-handling / reference…
tool result
biggest obj: qmetatype.cpp.o
--- target features ---
  7 linking         00041ce1 00000000 
 11 target_features 000000a4 00000000 
--- readobj features ---

File: qmetatype.cpp.o
Format: WASM
Arch: wasm32
AddressSize: 32bit
Version: 0x1
--- scan objects for try_table opcode ---
try_table across top30 objs: 5305
--- scan for invoke_ imports across top30 ---
invoke_ refs across top30 objs: 0
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
So the Qt toolchain at /opt/toolchains/qt-jspi is compiled with WASM-EH exception handling, which means it's using try_table instructions throughout the codebase rather than invoke_ references. This is the JSPI variant with wasm-exceptions support, which creates a compatibility issue—I can't easily link it with a JavaScript-based exception handling version of FreeCAD since the exception handling styles don't mix cleanly at the linking stage.

The core problem is that Emscripten requires a single exception handling mode across the entire binary because the C++ runtime needs to match. WASM-EH and JS-EH have incompatible ABIs for throw/catch operations, so mixing objects compiled with different  …
Fable
⚙ Bash {"command": "cd /tmp/qtjspi-eh\necho \"=== target_features content of qmetatype.cpp.o ===\"\n/opt/toolchains/emsdk/upstream/bin/llvm-objdump -s -j target_features qmetatype.cpp.o 2>/dev/null | head -20\necho\necho \"=== which Qt was FreeCAD…
tool result
=== target_features content of qmetatype.cpp.o ===

qmetatype.cpp.o:	file format wasm
Contents of section target_features:
 0000 0a2b0b62 756c6b2d 6d656d6f 72792b0f  .+.bulk-memory+.
 0010 62756c6b 2d6d656d 6f72792d 6f70742b  bulk-memory-opt+
 0020 1663616c 6c2d696e 64697265 63742d6f  .call-indirect-o
 0030 7665726c 6f6e672b 12657863 65707469  verlong+.excepti
 0040 6f6e2d68 616e646c 696e672b 0a6d756c  on-handling+.mul
 0050 74697661 6c75652b 0f6d7574 61626c65  tivalue+.mutable
 0060 2d676c6f 62  …[truncated]
Fable
thinking
So qt-jspi has the necessary features enabled for JSPI support with native WebAssembly exception handling and reference types, but FreeCAD's current build is still using qt-asyncify instead. Let me check the CMakeLists linking section to understand what flags are currently set and how the CPython trampoline workaround fits in.
Fable
⚙ Bash {"command": "sed -n '140,270p' /home/magik6k/lcad-wasm/freecad-port/FreeCAD/src/Main/CMakeLists.txt", "description": "Read FreeCAD Main CMakeLists link section"}
tool result
    set(FREECAD_WASM_EXTRA_LINK_LIBS "" CACHE STRING
        "Extra static archives appended to the FreeCAD wasm links")
    set(FREECAD_WASM_COMMON_LINK_OPTIONS
        "-sALLOW_MEMORY_GROWTH=1"
        # The indirect function table must grow at runtime: numpy's 12 C-extensions
        # plus the added workbenches register far more function pointers (asyncify
        # instruments every function) than fit the static table, so a fixed table
        # throws "WebAssembly.Table.grow() failed". All  …[truncated]
Fable
thinking
I'm reviewing my research notes on prior JSPI attempts and checking the emscripten version to understand the CPython trampoline and JSPI EXPORTED_FUNCTIONS mechanism. For Dimension B, I have solid ground on Qt, but I need to verify whether the jseh script actually produced a working build and if the JSPI plus JavaScript exception handling path is viable.
Fable
⚙ Bash {"command": "ls /home/magik6k/lcad-wasm/freecad-port/research/ 2>/dev/null\necho \"=== grep research for JSPI ===\"\ngrep -rlin \"jspi\" /home/magik6k/lcad-wasm/freecad-port/research/ 2>/dev/null", "description": "Find research notes on JSP…
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
=== grep research for JSPI ===
/home/magik6k/lcad-wasm/freecad-port/research/R10-eh-jspi-asyncify-findings.md
/home/magik6k/lcad-wasm/freecad-port/research/R2-dependency-matrix.md
/home/magik6k/lcad-wasm/freecad-port/research/R5-dialo  …[truncated]
Fable
⚙ Bash {"command": "echo \"############ R8-jspi-migration-plan.md ############\"; cat /home/magik6k/lcad-wasm/freecad-port/research/R8-jspi-migration-plan.md", "description": "Read JSPI migration plan research"}
tool result
############ R8-jspi-migration-plan.md ############
# R8 — JSPI Migration Plan (replace ASYNCIFY)

Status: **planned** (not yet executed). Prereq artifacts already exist:
`toolchain/rebuild-qt-jspi.sh`, and the code carries the ASYNCIFY↔JSPI notes.

## 1. Why (recap of the empirical finding)

The GUI currently links with `-sASYNCIFY` (Qt built `QT_HAVE_EMSCRIPTEN_ASYNCIFY`).
That boots the full GUI, but **Python→C calls made during the Qt event loop
crash in `_PyEM_TrampolineCall` ("unreachable"  …[truncated]
Fable
thinking
The R8 plan is actually older than the R10 finding about OCCT's wasm-EH miscompiling invalid br_table instructions. Since R10 is the newer blocker that halted the entire R8 plan, I need to review R10 to see where things stand now, particularly around the br_table issue and JSPI-across-JS-EH interaction. The task summary already covers R10's core points, so I'm focusing on checking whether Qt/JSPI verification has been addressed.
Fable
⚙ Bash {"command": "cat /home/magik6k/lcad-wasm/freecad-port/research/R10-eh-jspi-asyncify-findings.md", "description": "Read R10 EH/JSPI/asyncify findings"}
tool result
# R10 — Exceptions / JSPI / Asyncify: the definitive matrix

This documents an exhaustive investigation into the single hardest problem of the
port: making CPython's call trampoline, Qt's synchronous `exec()` event loop, and
OCCT's exception-heavy geometry kernel coexist in one wasm module. Every viable
combination of *exception model* × *stack-switching model* was built and tested.

## The three sub-problems (each forces a choice)

1. **CPython call trampoline.** CPython ≥3.13 calls `PyCFunctio  …[truncated]
Fable
thinking
Looking at the R10 matrix, I can see the core issue: JSPI combined with wasm-EH produces an invalid module due to an OCCT br_table miscompile, and that's exactly what the qt-jspi variant includes. The alternative of pairing JSPI with JS-EH links successfully but fails at runtime because JavaScript frames can't be suspended across invoke_ boundaries. For my Qt-specific work, I need to verify one critical detail that could resolve this.

The task is asking whether these blockers still hold in emscripten 4.0.12 with clang-22, though that's more about the compilation layer. For Qt specifically, I've confirmed the deliverables: Qt 6.11 has full JSPI support with runtime detection gated on wasm-ex  …
Fable
⚙ Bash {"command": "EM=/opt/toolchains/emsdk/upstream/emscripten\necho \"=== JSPI settings in settings.js ===\"\ngrep -n \"JSPI\" $EM/src/settings.js | head -40\necho\necho \"=== emscripten version ===\"\ncat $EM/emscripten-version.txt 2>/dev/null…
tool result
=== JSPI settings in settings.js ===
817:// - 2 (deprecated): Use ``-sJSPI`` instead.
931:// Deprecated, use JSPI_EXPORTS instead.
943:var JSPI = 0;
947:// that will call an asynchronous import (listed in ``JSPI_IMPORTS``) must be
952:var JSPI_EXPORTS = [];
962:var JSPI_IMPORTS = [];

=== emscripten version ===
"4.0.12"
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "EM=/opt/toolchains/emsdk/upstream/emscripten\nsed -n '930,975p' $EM/src/settings.js", "description": "Read JSPI_EXPORTS/JSPI_IMPORTS docs"}
tool result
// Deprecated, use JSPI_EXPORTS instead.
// [deprecated]
var ASYNCIFY_EXPORTS = [];

// 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]
  …[truncated]
Fable
thinking
I'm getting clarity on how Emscripten's JSPI model works in version 4.0.12—enabling it with a flag, then configuring which exports become Promise-returning functions that can suspend, with the default including `main` but requiring FreeCAD to explicitly add `_freecad_run_python` and other entry points that interact with Python or the Qt event loop. The async imports also get automatically marked through EM_ASYNC_JS functions.

Qt's suspend mechanism flows through EM_ASYNC_JS (qtSuspendJs), which becomes a suspending import under JSPI—that part checks out. But there's still the JS-EH invoke frame issue lurking in the OCCT/FreeCAD code path. I've already verified that the Qt platform plugin ha  …
Fable
⚙ Bash {"command": "echo \"=== qt-asyncify features ===\"\ngrep -i \"jspi\\|wasm.*exception\" /opt/toolchains/qt-asyncify/6.11.1/wasm_singlethread/mkspecs/qconfig.pri 2>/dev/null\necho \"--- qt-asyncify EH style (invoke_ vs try_table) on libQt6Cor…
tool result
=== qt-asyncify features ===
QT.global.disabled_features = shared pkg-config debug_and_release separate_debug_info appstore-compliant simulator_and_device rpath force_asserts framework c++20 c++2a c++2b c++2c reduce_relocations wasm-simd128 wasm-exceptions wasm-jspi zstd thread dbus openssl-linked opensslv11 opensslv30
--- qt-asyncify EH style (invoke_ vs try_table) on libQt6Core ---
invoke_ syms: 1408

=== Does emscripten auto-mark EM_ASYNC_JS as JSPI import? check library_async or jspi handlin  …[truncated]
Fable
thinking
I'm confirming the technical details across these build configurations: qt-asyncify uses wasm-jspi disabled with JavaScript exception handling but isn't compatible with JSPI, while qt-jspi has both wasm-jspi and wasm-exceptions enabled with proper exception handling and a complete build including all libraries and the platform plugin. Emscripten 4.0.12 supports JSPI through the appropriate compiler flags.

Now I have the answers for this dimension and need to structure the output. The core issue is whether JS-EH can suspend across invoke_ calls in the latest emscripten version—this is the critical blocker determining whether a JS-EH JSPI path could work with the current OCCT setup. I should   …
Fable
Let me run the decisive test for the JS-EH + JSPI path (whether the jseh Qt variant could ever work) in an isolated dir.
Fable
⚙ Bash {"command": "source /home/magik6k/lcad-wasm/env.sh 2>/dev/null\nmkdir -p /tmp/jspi-jseh-test && cd /tmp/jspi-jseh-test\ncat > t.cpp <<'EOF'\n#include <emscripten.h>\n#include <cstdio>\n// async import that suspends (becomes WebAssembly.Susp…
tool result
em++: warning: -sASYNCIFY=2 (JSPI) is still experimental [-Wexperimental]
=== EXIT compile: 0 ===
-rw-r--r-- 1 magik6k magik6k 42375 Jul  6 01:08 t.js
-rwxr-xr-x 1 magik6k magik6k 24431 Jul  6 01:08 t.wasm
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "source /home/magik6k/lcad-wasm/env.sh 2>/dev/null\ncd /tmp/jspi-jseh-test\ncat > run.mjs <<'EOF'\nimport Mod from './t.js';\nconst m = await Mod();\ntry {\n const p = m.ccall('run', 'number', [], [], {async: true});\n const r…
tool result
/tmp/jspi-jseh-test/t.js:968
              imports[x] = original = new WebAssembly.Suspending(original);
                                      ^

TypeError: WebAssembly.Suspending is not a constructor
    at Object.instrumentWasmImports (/tmp/jspi-jseh-test/t.js:968:39)
    at getWasmImports (/tmp/jspi-jseh-test/t.js:454:12)
    at createWasm (/tmp/jspi-jseh-test/t.js:499:14)
    at /tmp/jspi-jseh-test/t.js:1253:25
    at file:///tmp/jspi-jseh-test/run.mjs:2:17
    at ModuleJob.run (node:interna  …[truncated]
Fable
thinking
Node 22 switched to a new JSPI API with `WebAssembly.Suspending` and `WebAssembly.promising`, which requires the `--experimental-wasm-jspi` flag instead of the older `--experimental-wasm-stack-switching`. Let me test with the updated flag.
Fable
⚙ Bash {"command": "cd /tmp/jspi-jseh-test\necho \"=== try --experimental-wasm-jspi ===\"\nnode --experimental-wasm-jspi run.mjs 2>&1 | tail -20\necho \"=== exit: $? ===\"", "description": "Run with correct JSPI flag"}
tool result
=== try --experimental-wasm-jspi ===
node: bad option: --experimental-wasm-jspi
=== exit: 0 ===
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
⚙ Bash {"command": "cd /tmp/jspi-jseh-test\nnode --v8-options 2>/dev/null | grep -i \"jspi\\|stack.switch\\|suspend\" | head\necho \"=== try --experimental-wasm-jspi as v8 flag ===\"\nnode --js-flags=\"--experimental-wasm-jspi\" run.mjs 2>&1 | tai…
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
In Node 22.16 with V8, JSPI appears to be stabilized and enabled by default now, but the error about `WebAssembly.Suspending` not being a constructor suggests it's still not available in the runtime. Let me verify whether JSPI is actually enabled or if there's a different flag needed to access it.
Fable
⚙ Bash {"command": "cd /tmp/jspi-jseh-test\necho \"=== default ===\"; node -e \"console.log('Suspending:', typeof WebAssembly.Suspending, 'promising:', typeof WebAssembly.promising)\"\necho \"=== with stack-switching flag ===\"; node --experimenta…
tool result
=== default ===
Suspending: function promising: function
=== with stack-switching flag ===
node: bad option: --experimental-wasm-stack-switching
=== list jspi-ish flags ===
  --stress-wasm-stack-switching (Always run wasm on a secondary stack, even when it is called with a regular (non-JSPI) export)
  --experimental-wasm-growable-stacks (enable growable stacks for jspi for Wasm (experimental))
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
JSPI is enabled by default in Node 22.16, so I don't need any experimental flags. The earlier error about `WebAssembly.Suspending` not being a constructor happened because I used the `--experimental-wasm-stack-switching` flag, which is no longer valid in this version and caused Node to error out.
Fable
⚙ Bash {"command": "cd /tmp/jspi-jseh-test\necho \"=== run with NO flags (JSPI default-on) ===\"\nnode run.mjs 2>&1 | tail -20\necho \"=== exit: $? ===\"", "description": "Run JSPI+JS-EH test with default JSPI"}
tool result
=== run with NO flags (JSPI default-on) ===
THREW: SuspendError: trying to suspend without WebAssembly.promising
=== exit: 0 ===
zshexit:1: parameter HISTDB_FD does not contain a file descriptor
Fable
thinking
I'm seeing the R10 blocker error about suspending without WebAssembly.promising. The `_run` export should be wrapped by emscripten since it's in JSPI_EXPORTS, so I need to figure out whether the problem is in my ccall async harness or in the actual invoke_ frame. The error happens when a suspend occurs on a stack where the entry export wasn't properly created with promising support.

The issue is that the try/catch introduces an `invoke_` call — a JS function that calls back into wasm via a function pointer. This JS frame becomes a new stack entry that isn't promising-wrapped, so when do_suspend tries to suspend, it fails crossing that invoke_ boundary. This matches exactly what R10 found. I  …
Fable
⚙ Bash {"command": "source /home/magik6k/lcad-wasm/env.sh 2>/dev/null\ncd /tmp/jspi-jseh-test\n# Control: same everything but NO throw/catch (no invoke_ frame)\ncat > t2.cpp <<'EOF'\n#include <emscripten.h>\n#include <cstdio>\nEM_ASYNC_JS(void, do…
tool result
=== CONTROL (no try/catch): ===
    at ModuleJob.run (node:internal/modules/esm/module_job:274:25)
    at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26)
    at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5)

Node.js v22.16.0

=== Now confirm the failing one had _run promising-wrapped. grep t.js for promising ===
968:              imports[x] = original = new WebAssembly.Suspending(original);
985:            // Wrap all exports with a   …[truncated]
Fable
thinking
I need to check the full control output since it was truncated — let me get the complete error message to see what actually went wrong.
Fable
⚙ Bash {"command": "cd /tmp/jspi-jseh-test\necho \"=== CONTROL full output ===\"; node run2.mjs 2>&1 | grep -iE \"RESULT|THREW|resumed|Error|suspend\" | head\necho\necho \"=== how t.js wraps exports (985-1015) ===\"\nsed -n '985,1016p' t.js", "des…
tool result
=== CONTROL full output ===
suspending (no try/catch)...
THREW: SuspendError: trying to suspend without WebAssembly.promising

=== how t.js wraps exports (985-1015) ===
            // Wrap all exports with a promising WebAssembly function.
            let isAsyncifyExport = exportPattern.test(x);
            if (isAsyncifyExport) {
              Asyncify.asyncExports.add(original);
              original = Asyncify.makeAsyncFunction(original);
            }
            var wrapper = Asyncify.ins  …[truncated]
Fable
thinking
The test harness itself is the problem — when I use ccall with async:true, it's not actually invoking the WebAssembly.promising wrapper that _run is exported with, so the suspension fails. Under JSPI, the export is already the promising function, but ccall might be calling the raw export instead of going through the wrapper.

I think the issue is that my JSPI_EXPORTS pattern isn't matching _run correctly — Emscripten might expect the name without the leading underscore. Let me just call the export directly and await it rather than going through ccall, and make sure the JSPI_EXPORTS configuration is right.
Fable
⚙ Bash {"command": "source /home/magik6k/lcad-wasm/env.sh 2>/dev/null\ncd /tmp/jspi-jseh-test\ngrep -n \"exportPattern\" t2.js | head\n# call the promising export directly instead of ccall\ncat > run3.mjs <<'EOF'\nimport Mod from './t2.js';\nconst…
tool result
935:        var exportPattern = /^(_run|main|__main_argc_argv)$/;
941:            let isAsyncifyExport = exportPattern.test(x);
              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
There's something inconsistent happening between run2.mjs and run3.mjs — both import the same t2.js file, but run2 gets past the import while run3 fails immediately with "WebAssembly.Suspending is not a constructor." I'm wondering if Node's JSPI support depends on a specific flag or origin trial being enabled, which would explain why Suspending is available as a function in the default environment but not consistently across different invocations.

Looking at the export pattern, `_run` should be matched and promise-wrapped, which is good. The control test (run2) fails with "trying to suspend without WebAssembly.promising," which tells me the promise-wrapped export isn't establishing the prom  …
Fable
I have all the Dimension B facts. Let me do one final confirmation on the qt-jspi platform plugin completeness and then report.
Fable
⚙ Bash {"command": "echo \"=== qt-jspi platform plugin object EH + jspi ===\"\ncd /tmp && rm -rf qwasmchk && mkdir qwasmchk && cd qwasmchk\n/opt/toolchains/emsdk/upstream/bin/llvm-ar x /opt/toolchains/qt-jspi/6.11.1/wasm_singlethread/plugins/platf…
tool result
=== qt-jspi platform plugin object EH + jspi ===
jspi-suspend syms in plugin: 5
=== newest of the 3 qt-jspi scripts (which built installed one) ===
-rw------- 1 magik6k magik6k 2652 04:58 /opt/toolchains/rebuild-qt-jspi-jseh.sh
-rwx------ 1 magik6k magik6k 2316 02:41 /opt/toolchains/rebuild-qt-jspi.sh
-rw------- 1 magik6k magik6k 2683 05:43 /opt/toolchains/rebuild-qt-jspi-wasmeh.sh
installed qt-jspi libQt6Core.a mtime:
-rw------- 1 magik6k magik6k 10514546 05:44 /opt/toolchains/q  …[truncated]
Fable
thinking
The Qt JSPI build with wasm-EH is confirmed installed and matches the rebuild script, with the plugin containing the expected JSPI suspend symbols. Now I'm ready to construct the StructuredOutput.
Fable
I have all the facts for Dimension B. Compiling the report.
Fable
⚙ StructuredOutput {"dimension": "B — Qt-for-wasm JSPI support + what must be rebuilt/changed on the Qt and FreeCAD-link side", "findings": ["Qt 6.11.1 has FULL, mature JSPI support — not a stub. Configure exposes `-feature-wasm-jspi` (QT_FEATURE_wasm_jspi / …
tool result
Structured output provided successfully