{
 "summary": "First-ever -sJSPI builds of this codebase were spiked successfully: emsdk 6.0.6 (installed side-by-side in scratchpad; repo pins 4.0.2) linked four wx test apps from tests/apps/Makefile.wasm against the prebuilt 4.0.2-era wx static libs, and they RUN in Chromium 143 (JSPI default-on) and Firefox 144 (behind pref). Verified wins: suspend-inside-C++-catch works natively (obsoletes the 407-line HoistCppCatches binaryen fork pass), wasm is 2.14x smaller than the post-link asyncify pipeline output (4.41MB vs 9.46MB on identical input), the ~27s/app wasm-opt post-link stage disappears, embind async() works, pthreads (16 threads + worker-thread emscripten_sleep) work, mimalloc/-gseparate-dwarf/legacy-EH all link and mostly run. Verified clashes: (1) -sDYNCALLS=1 is a hard link error under JSPI (but it exists only to serve the asyncify pipeline, so it can be dropped along with inject-dyncall-shims.sh); (2) every wasm entry export that can transitively suspend must be declared in JSPI_EXPORTS or it throws SuspendError at runtime \u2014 an enumerable, bounded list (9 exports in the tested port, 18 in the current one), and with the list supplied, clipboard copy and a full modal-dialog open/close cycle pass; (3) emscripten fibers \u2014 the backend of KiCad's libcontext coroutines \u2014 are runtime-broken under JSPI (Asyncify.State undefined), making the coroutine layer the single hard migration item; (4) JSPI also legalizes overlapping suspended calls (reentrancy max=2 observed), eliminating the \"cannot start an async operation when one is already in flight\" abort class (reproduced live in the asyncify control run) but making reentrancy-serialization a design responsibility instead of an engine constraint.",
 "findings": [
  {
   "title": "Spike provenance: what was built, with what, against what",
   "detail": "emsdk 6.0.6 (latest; list showed 5.0.0..6.0.6) installed side-by-side into the session scratchpad (repo untouched; repo pins EMSCRIPTEN_VERSION=4.0.2). Because the pcbjam-private checkout has no built wx libraries (pcbjam/build-wasm/wxwidgets/lib holds only wx/ config headers; pcbjam/tools/emsdk has no binaries), the spike linked against the sibling full checkout /Users/V/IdeaProjects/kicad-wasm (same app repo, branch main, HEAD a35eeb7e; wxwidgets a61bcf4 = v3.2.6-92, i.e. 37 port commits behind pcbjam's 4d479cb v3.2.6-129; wx libs built with emcc 4.0.2). Flags mirrored tests/apps/Makefile.wasm: EH_FLAGS -fwasm-exceptions -sSUPPORT_LONGJMP=wasm -sWASM_LEGACY_EXCEPTIONS=1 (-sDYNCALLS dropped, see clash), -sALLOW_MEMORY_GROWTH, -sERROR_ON_UNDEFINED_SYMBOLS=0, wx-config cxxflags/libs (which force -pthread into every app), --pre-js wx.js + wx-dom.js, with -sASYNCIFY=1/-sASYNCIFY_STACK_SIZE=65536/-sASYNCIFY_IMPORTS replaced by -sJSPI + -sJSPI_IMPORTS + -sJSPI_EXPORTS. Cross-version object compat verified incidentally: 6.0.6 linker consumed 4.0.2-compiled .a/.o (including EM_JS/EM_ASYNC_JS custom sections) with zero errors. Zero prior JSPI usage in build scripts confirmed (grep JSPI over pcbjam/scripts + Makefile.wasm = 0 hits; docs-only mentions in docs/README.md, docs/wasm-exceptions-experiment.md, docs/research/threading_2.md).",
   "evidence": "pcbjam/scripts/common/versions.sh:6; pcbjam/tests/apps/Makefile.wasm:58,66-70,95-98,101-105,119-124; kicad-wasm wx-config output includes -pthread in both --cxxflags and --libs; artifacts in scratchpad jspi-spike/out/",
   "jspi_impact": "neutral \u2014 establishes the evidence base; caveat: wx libs tested are 37 commits behind the current port."
  },
  {
   "title": "HARD CLASH (removable): -sDYNCALLS=1 is a fatal link error under -sJSPI",
   "detail": "First JSPI link attempt with the Makefile's exact EH_FLAGS died with: AssertionError \"DYNCALLS cannot be used with JSPI\" at makeDynCall (emscripten src/parseTools.mjs:693) while preprocessing libpthread.js \u2014 an internal compiler error, not a graceful diagnostic. The repo passes -sDYNCALLS=1 in both the wx test flags and the production KiCad link. But DYNCALLS exists ONLY to serve the asyncify pipeline: inject-dyncall-shims.sh documents that asyncify-INSTRUMENTED dynCall_* trampolines must be used for unwind/rewind through indirect calls. Under JSPI there is no unwind/rewind instrumentation, so the flag, the wasmExports[\"dynCall_\"+sig] routing, the embind dynCall fallback perl patch, and the -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['$dynCall'] export can all be deleted. Relinking without -sDYNCALLS succeeded immediately.",
   "evidence": "error reproduced with emsdk 6.0.6; pcbjam/tests/apps/Makefile.wasm:58; pcbjam/scripts/kicad/build-kicad-target.sh:554; pcbjam/scripts/common/inject-dyncall-shims.sh:13-19,114-122; emscripten src/parseTools.mjs:693",
   "jspi_impact": "clash \u2014 but a one-line flag removal plus deletion of a whole shim script; net simplification."
  },
  {
   "title": "HARD CLASH (architectural, bounded): every suspending wasm entry export must be a JSPI_EXPORT; default builds throw SuspendError on first UI interaction",
   "detail": "With only default JSPI_EXPORTS (main), clipboard and dialog apps boot but the first button click fails: \"SuspendError: trying to suspend without WebAssembly.promising\" thrown from wasm frames entered via wx_dom_event \u2014 the DOM port dispatches ALL UI events through plain ccall('wx_dom_event',...) (wx-dom.js:46), and under JSPI only exports wrapped by WebAssembly.promising may suspend. Side effect observed: the failed suspension left wxClipboard open (\"wxClipboard::Open() called when already open\" on retry) \u2014 errors mid-suspend corrupt C++ state. FIX VERIFIED: relinking with -sJSPI_EXPORTS=['main','wx_dom_event','wx_dom_mouse','wx_window_close','wx_window_move','wx_window_resize','ProcessEvents'] made both scenarios pass end-to-end: clipboard \"SUCCESS: Copied 30 characters\" (EM_ASYNC_JS js_writeTextToClipboard suspending inside a DOM click handler), and a full modal cycle \u2014 Info dialog rendered as DOM, OK clicked, \"Info dialog closed with result: 4\", main loop resumed (startModal EM_ASYNC_JS at wxwidgets/src/wasm/dialog.cpp:201 suspending main). The migration surface is enumerable: 9 EMSCRIPTEN_KEEPALIVE entries in the tested 92-commit port; 18 in the current 129-commit port (wx_dom_event, wx_dom_mouse, wx_window_close/move/resize, drag/drop handlers, ProcessEvents, wxWasmMailboxTick, wxWasmMainLoopPump, wxWasmSched* family, wxWasmTopLevelTick, wx_dispatch_abandon). JS callers already tolerate promise returns (ProcessEvents is even called with ccall {async:true} today, wx-dom.js:1089). Note: exports listed in JSPI_EXPORTS return Promises to JS; wasm size unchanged by the wrapping.",
   "evidence": "runtime errors + fixes captured in results.json/results2.json (scratchpad jspi-spike/); kicad-wasm wxwidgets/build/wasm/wx-dom.js:46,1089; wxwidgets/src/wasm/domevents.cpp:87,116; KEEPALIVE grep over pcbjam/wxwidgets/src/wasm/*.cpp (18 symbols); emscripten settings.js:952 (JSPI_EXPORTS)",
   "jspi_impact": "clash \u2014 the central code change of a JSPI port, but bounded, mechanical, and proven working in the spike."
  },
  {
   "title": "WIN (verified): JSPI suspends inside C++ catch blocks natively \u2014 the 407-line HoistCppCatches binaryen fork pass becomes unnecessary",
   "detail": "Custom feature harness (compiled+linked wholly with 6.0.6, -pthread, -fwasm-exceptions, -sWASM_LEGACY_EXCEPTIONS=1, -sJSPI): (1) suspend_in_catch \u2014 throw 42, then INSIDE the catch arm call EM_ASYNC_JS js_delay(50) AND emscripten_sleep(10), return e+r \u2192 returned 142 (correct) in Chromium; this is exactly the case Asyncify cannot handle and the reason the binaryen fork exists (apply-asyncify.sh: \"lets Asyncify suspend from inside C++ catch blocks under native wasm-EH\"). (2) throw_across_suspend \u2014 suspend inside try then throw/catch \u2192 20 (correct). (3) sjlj_roundtrip under -sSUPPORT_LONGJMP=wasm \u2192 7 (correct). Consequence: the binaryen submodule fork (version_130 + HoistCppCatches), the wasm-opt stubbing machinery in build-wasm-test.sh/build-kicad-target.sh, apply-asyncify.sh, asyncify-imports.txt (boundary list) and asyncify-removelist.txt (71 lines, RAM-blowup mitigation) are all dead weight under JSPI.",
   "evidence": "feature harness results3.json: {suspend_in_catch:{v:142,ok:true},throw_across_suspend:{v:20,ok:true},sjlj_roundtrip:{v:7,ok:true}}; pcbjam/scripts/common/apply-asyncify.sh:6-19; pcbjam/scripts/common/asyncify-removelist.txt (71 lines); harness source scratchpad jspi-spike/feature_harness.cpp",
   "jspi_impact": "win \u2014 eliminates the entire custom binaryen fork + post-link pipeline raison d'\u00eatre."
  },
  {
   "title": "HARD CLASH (the big one): emscripten fibers \u2014 the backend of KiCad's libcontext coroutines \u2014 are runtime-broken under JSPI",
   "detail": "KiCad's coroutine layer (thirdparty/libcontext/libcontext.cpp:20-24 \"WASM/Emscripten: implement libcontext using Emscripten fibers\", emscripten_fiber_init/swap at :210,:258,:287,:321) drives the tool framework. The coroutine stress harness (tests/apps/standalone/coroutine, links real libcontext) LINKS under -sJSPI without warning \u2014 emscripten 6.0.6 even includes the real Fibers JS implementation, not the abort stub \u2014 but at runtime the FIRST case dies: pageerror \"TypeError: Cannot read properties of undefined (reading 'Normal')\" at generated coroutine_jspi.js:9563 \"if (Asyncify.state === Asyncify.State.Normal)\" \u2014 the JSPI variant of the Asyncify runtime object has no State/state members; Fibers is functionally ASYNCIFY=1-only (upstream guard bug: fibers ship under any ASYNCIFY truthy value). Only 1 of the harness cases even started ([COROUTINE_TEST] CASE first_entry_runs_once, no PASS/SUMMARY ever printed). There is no JSPI fiber backend upstream: JSPI suspends toward JS only; it cannot do arbitrary wasm-stack-to-wasm-stack switching. Migration options are all invasive: rewrite libcontext-on-wasm as promising-export trampolines through JS, restructure KiCad tool coroutines, or thread-backed coroutines.",
   "evidence": "pageerror + console in results.json (coroutine_jspi); generated coroutine_jspi.js:9525-9580 (Fibers, Asyncify.State refs); kicad-wasm/kicad/thirdparty/libcontext/libcontext.cpp:20-24,210,258,287,321; emscripten src/lib/libasync.js:577-580 (real fiber impl) vs :625 (abort stub only when ASYNCIFY=0); pcbjam Makefile.wasm:105,124 (fiber_swap in ASYNCIFY_IMPORTS)",
   "jspi_impact": "clash \u2014 the single hard blocker; everything else in the spike passed, this did not."
  },
  {
   "title": "WIN (verified): pthreads + JSPI work \u2014 16 real threads, main-thread blocking join, and emscripten_sleep on a worker thread",
   "detail": "All wx apps link -pthread (wx-config forces it). threadpool_test (replicates KiCad's BS::priority_thread_pool: create hardware_concurrency std::threads, join all on the main thread) built with -sJSPI -sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency -sPTHREAD_POOL_SIZE_STRICT=0: \"Thread Pool Test PASSED! Created and joined 16 threads successfully\" \u2014 page completed in 414ms wall. Feature harness start_worker_sleep: a detached std::thread called emscripten_sleep(50) then set a flag \u2192 flag observed 1 (worker-thread JSPI suspension works in Chromium 143). Production's no-PROXY_TO_PTHREAD main-thread-main() model (build-kicad-target.sh:532 comment) maps directly onto JSPI's promising main. Reentrancy probe: two overlapping promising calls into the same export ran concurrently (first call saw depth 1, second saw depth 2, reentry_max=2) \u2014 JSPI permits multiple suspended activations per thread. This ELIMINATES the \"Aborted(Assertion failed: We cannot start an async operation when one is already flight)\" failure class \u2014 which the asyncify CONTROL run reproduced live during the clipboard A/B \u2014 but means the scheduler/mailbox serialization (docs/features/async/17-22 machinery) becomes a policy layer, not an engine-enforced constraint: true reentrancy is now the DEFAULT semantic on every suspending entry.",
   "evidence": "results2.json threadpool_jspi (PASSED, 414ms, 0 errors); results3.json feature harness {worker_sleep:{flag:1},reentrancy:{v1:1,v2:2,max:2}}; asyncify control abort in results3.json clipboard_asyncify_control; pcbjam/scripts/kicad/build-kicad-target.sh:531-539",
   "jspi_impact": "win on threads; mixed on reentrancy \u2014 kills the in-flight abort class the 107 commits fought, but requires an explicit serialization decision per entry point."
  },
  {
   "title": "WIN (verified): embind async bindings are JSPI-native; EM_ASYNC_JS auto-wrapping works, including from 4.0.2-era objects",
   "detail": "Feature harness bound embind_async_fn (calls emscripten_sleep + EM_ASYNC_JS) with emscripten::async() policy (wire.h:547 struct async) and embind_sync_fn plainly: Module.embindAsyncFn(10) returned a Promise resolving to 40 (correct), embindSyncFn(4)=5 (correct). Upstream 6.0.6 libembind.js asserts async bindings are ONLY supported with JSPI (\"assert(!isAsync, 'async bindings are only supported with JSPI')\" under ASYNCIFY!=2) \u2014 i.e. the production embind TUs (wasm/bindings/*_embind.cpp, linked at build-kicad-target.sh:554 via --bind) gain a first-class async story only by moving to JSPI; today they rely on Asyncify.currData.then plumbing. EM_ASYNC_JS auto-marking as suspending imports (__asyncjs__ prefix) worked without listing them in JSPI_IMPORTS \u2014 including EM_ASYNC_JS bodies embedded in libwx_wasmu_core-3.2.a objects compiled by emcc 4.0.2 and consumed by the 6.0.6 -sJSPI link (startModal, js_writeTextToClipboard/js_readTextFromClipboard/js_clipboardHasText/js_clearClipboard).",
   "evidence": "results3.json {embind_sync:{v:5,ok:true},embind_async:{v:40,ok:true}}; emscripten src/lib/libembind.js:696-698,769-772; system/include/emscripten/wire.h:547,659; kicad-wasm wxwidgets/src/wasm/dialog.cpp:201, clipbrd.cpp:38-147 (EM_ASYNC_JS); tools/emscripten.py:804 (MAIN_MODULE or ASYNCIFY==2 em_js handling)",
   "jspi_impact": "win \u2014 embind + EM_ASYNC_JS need no per-function code change beyond the async() policy on suspending bindings."
  },
  {
   "title": "WIN (measured): wasm 2.14x smaller than the post-link asyncify pipeline and the ~27s/app wasm-opt stage disappears",
   "detail": "Identical input (minimal_test.o + wx libs, emcc 6.0.6, -O2): plain link (no suspend support) = 3,922,345 B wasm, 1.58s. JSPI link = 4,414,628 B wasm (+12.6% over plain) / 425,457 B js, link 0.7-3.0s, NO post-link steps. Production-style post-link pipeline replicated on the same plain input with the repo's fork wasm-opt (version_130-2-g1d40cf5a8 from kicad-wasm/build-wasm/tools/binaryen-hoist-build/bin): --hoist-cpp-catches 0.23s + --asyncify (asyncify-imports.txt boundary, no removelist, propagate-addlist) 1.03s + -O2 25.8s (1m50s CPU at BINARYEN_CORES=8) = 9,457,996 B (2.14x JSPI, +141% over plain) \u2014 closely matching the shipped 4.0.2-era baseline minimal_test.wasm of 9,589,611 B (Jul 20 build). In-link ASYNCIFY=1 under 6.0.6 for reference: 14,646,061 B / 450,062 B js, 6.1s. Per build-wasm-test.sh's own comment the per-app wasm-opt stage \"dominates the build\" (clean test-suite build 15m36s at -j1); JSPI deletes that stage entirely, plus ASYNCIFY_STACK_SIZE tuning, the imports boundary file, and the 71-line removelist (whose RAM-blowup problem no longer exists). Caveat: JSPI disables wasm import/export minification (link.py:1656 TODO) \u2014 part of the +12.6% over plain.",
   "evidence": "size/time numbers from spike runs (scratchpad jspi-spike/out/); kicad-wasm/tests/apps/minimal_test.wasm = 9,589,611 B (Jul 20); pcbjam/scripts/build-wasm-test.sh:7-9,144-146; pcbjam/scripts/common/apply-asyncify.sh:122-137; emscripten tools/link.py:1656-1663",
   "jspi_impact": "win \u2014 ~5MB wasm saved per app at minimal_test scale (KiCad editors are 10-17MB+ instrumented today), plus minutes of wasm-opt per app per build."
  },
  {
   "title": "Runtime matrix: Chromium 143 passes everything (except fibers); Firefox 144 works only behind a pref; legacy wasm-EH is fine with JSPI; exnref requires full recompile but is NOT needed",
   "detail": "Chromium 143.0.7499.4 (Playwright 1.57 bundled; JSPI on by default since Chrome 137): minimal (21 DOM buttons, click handled), clipboard, dialog modal cycle, threadpool, feature harness \u2014 all pass. Firefox 144.0.2 with javascript.options.wasm_js_promise_integration=true: minimal boots fully (only a deprecation warning about legacy EH 'try' instructions); WITHOUT the pref: clean abort \"Assertion failed: JSPI not supported by current environment\" (emscripten's 'Suspending' in WebAssembly feature check, libasync.js:52) \u2014 JSPI is NOT default-on in Firefox 144. Legacy-EH encoding (-sWASM_LEGACY_EXCEPTIONS=1, required to stay compatible with the 4.0.2-built libraries) runs fine under JSPI in both browsers \u2014 the fork's env.sh:45 claim \"Asyncify can't handle exnref\" simply stops mattering. Attempting -sWASM_LEGACY_EXCEPTIONS=0 at link over legacy-compiled objects produced a wasm that Chrome REJECTS at compile: \"module uses a mix of legacy and new exception handling instructions\" \u2014 the exnref translator does not fully convert 4.0.2-era objects, so an exnref migration means recompiling every TU; it is optional, not a JSPI prerequisite. Also verified linking: -sMALLOC=mimalloc + JSPI and -g -gseparate-dwarf + JSPI both link (dbg: 5,212,199 B wasm + 43,118,462 B .debug.wasm).",
   "evidence": "results.json entries chromium/*, firefox+pref, firefox-nopref; browser versions from results2.json (143.0.7499.4 / 144.0.2); emscripten src/lib/libasync.js:52; pcbjam/scripts/common/env.sh:39-46; exnref CompileError text captured verbatim",
   "jspi_impact": "win/neutral \u2014 the accepted-risk browser matrix is confirmed empirically: Chrome-family fine, Firefox needs a pref today, no exnref migration required."
  },
  {
   "title": "Flagged non-JSPI issue: mimalloc OOB under emcc 6.0.6 in BOTH asyncify and JSPI builds; also, in-link asyncify no longer crashes on legacy wasm-EH",
   "detail": "minimal_test + -sMALLOC=mimalloc hits pageerror \"RuntimeError: memory access out of bounds\" after UI construction in BOTH the -sJSPI build and the -sASYNCIFY=1 in-link 6.0.6 build (and with -sASSERTIONS=1) \u2014 so it is NOT JSPI-specific; note the spike omitted production's mimalloc accompaniments (mallinfo stub, nanosleep-yield shim \u2014 build-kicad-target.sh:519,532; MEMORY.md flags mi_atomic_yield/nanosleep interplay). Needs separate root-causing before any emsdk upgrade regardless of JSPI. Second side-finding: emcc 6.0.6's in-link ASYNCIFY=1 on legacy wasm-EH input LINKS and BOOTS minimal_test (emits warning \"ASYNCIFY=1 is not compatible with -fwasm-exceptions. Parts of the program that mix ASYNCIFY and exceptions will not compile\") \u2014 the \"emsdk-bundled Binaryen crashes asyncifying wasm-EH\" rationale (apply-asyncify.sh:12-16, from emsdk v121 era) is outdated for crash behavior, though upstream still cannot suspend-in-catch and produced a 14.6MB wasm, so it does not replace the fork for the Asyncify path. Clipboard A/B: JSPI build copied 30 chars and pasted 1 char (\"S\"); the asyncify CONTROL (shipped Jul 20 pipeline build) pasted 0 chars AND aborted with the in-flight assertion \u2014 headless-clipboard fidelity is environmental/inconclusive, but JSPI strictly outperformed the control.",
   "evidence": "results.json/results3.json mimalloc entries (identical OOB both modes); asyncify-inlink warning text verbatim from build log; results3.json clipboard_asyncify_control vs clipboard_jspi2_again; pcbjam/scripts/kicad/build-kicad-target.sh:519,529-539",
   "jspi_impact": "neutral \u2014 mimalloc OOB is an emsdk-upgrade risk independent of JSPI; the in-link-asyncify datapoint slightly weakens the case for keeping the fork even without JSPI."
  }
 ],
 "open_questions": [
  "Fibers/libcontext replacement design: JSPI cannot stack-switch wasm-to-wasm, and emscripten 6.0.6 has no JSPI fiber backend (Fibers JS references Asyncify.State which doesn't exist under JSPI). What replaces KiCad's coroutine layer \u2014 promising-export trampolines per coroutine, thread-backed coroutines, or restructuring the tool framework? This is the gating item for any JSPI decision and needs its own spike on real pcbnew tool code.",
  "Full KiCad-scale JSPI link not yet attempted: pcbjam-private has no built wx/deps sysroot on this machine and the kicad build would need all deps recompiled under a JSPI-capable emsdk (6.0.6 linking 4.0.2 objects worked for wx test apps, so an incremental relink of the existing kicad-kicad_editor objects + sysroot from /Users/V/IdeaProjects/kicad-wasm may be feasible as a next step; embind TUs, GL, OCC untested at scale).",
  "Current 129-commit port surface: the spike used the 92-commit wx libs (a61bcf4). The current port's scheduler (wxWasmYieldUntil/yieldwait.h, __wxScheduler.resolveWait EM_JS token parks, 18 KEEPALIVE entries incl. wxWasmSched* family) must map its park import onto an EM_ASYNC_JS/promise-returning import and enlarge JSPI_EXPORTS \u2014 mechanically similar to the verified wx_dom_event fix, but unverified; equally unverified is whether reverting to 9ece9844 (pre-scheduler) is the cheaper JSPI base, which the reentrancy result (overlapping suspended calls are legal by default) makes plausible.",
  "mimalloc 'memory access out of bounds' under emcc 6.0.6 (both asyncify and JSPI, with and without assertions) \u2014 root cause needed before any emsdk upgrade; retest with production's mallinfo stub + nanosleep-yield shim linked.",
  "Headless clipboard paste fidelity (JSPI pasted 1 char, asyncify control pasted 0 chars and aborted): retest headed/real-Chrome to separate environment flakiness from a possible heap-view-after-growth issue during JSPI suspension.",
  "Firefox timeline for default-on JSPI (144.0.2 still requires javascript.options.wasm_js_promise_integration=true) and whether the legacy-EH 'try' deprecation warning there becomes a removal that would force the full exnref recompile.",
  "Emscripten labels JSPI 'still experimental' (link.py:1786) and JSPI disables wasm export-name minification (link.py:1656 TODO) \u2014 track upstream stabilization; also confirm behavior on installed stable Chrome 151 (spike browsers were Playwright-bundled Chromium 143/Firefox 144)."
 ]
}