feat: WASI port V2 — combined branch: #2's runtime + #1's test scenarios + API-contract tests - #3
feat: WASI port V2 — combined branch: #2's runtime + #1's test scenarios + API-contract tests#3scottmarchant wants to merge 17 commits into
Conversation
Co-authored-by: Scott Marchant <15382220+scottmarchant@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Scott Marchant <15382220+scottmarchant@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Scott Marchant <15382220+scottmarchant@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Register the single-thread-compatible subset of the upstream bsdtests suite for WASI instead of relying only on the bespoke tests/wasm suite. Test binaries are executed by a new WASI_TEST_RUNNER cache variable (default wasmtime); when the runner is absent the tests are registered but disabled so configuration still succeeds. bsdtestharness is not built for WASI because posix_spawn does not exist there; the runner propagates the guest exit code instead. Compiling bsdtests and the tests for wasm32-wasip1 needs __wasi__ arms next to the existing __unix__ guards (WASI clang does not define __unix__), the generic_unix_port.h shims, a WASI-safe failure exit status (WASI rejects 0xff), and stubs for the large-file helpers since wasi-libc has no mkstemp. 11 of the 20 default DISPATCH_C_TESTS plus dispatch_c99 and dispatch_plusplus pass under wasmtime. The remaining 9 need concurrent worker threads or file-descriptor sources and are excluded with the reason documented in tests/CMakeLists.txt. Co-authored-by: Scott Marchant <15382220+scottmarchant@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fold the best of the first WASI port candidate (PR #1) and the findings from the side-by-side comparison of both candidates into the test suite: - sync-inline.c, main-queue-order.c: inline dispatch_sync without a drain and thread-bound main-queue FIFO ordering, adapted from PR #1's dispatch_wasi_sync and dispatch_wasi_mainqueue tests. - blocking-waits.c: pins the blocking-wait contracts that PR #1's trap-on-block design violated: dispatch_block_wait runs the queued block, dispatch_group_wait(FOREVER) returns once the group empties, and a timed semaphore wait consumes its full timeout instead of returning early. - api-surface.c: one-binary sweep of the object/attr/block/data/group/ source families, derived from the probe program used to compare the two candidates. - assert-queue.c: dispatch_assert_queue must trap off-queue and pass on-queue. Guards the tid-vs-DLOCK_OWNER_MASK encoding in shims/lock.h; PR #1 shipped an unshifted constant tid that masked to DLOCK_OWNER_NULL, making assert_queue pass off-queue and assert_queue_not trap spuriously. All 35 ctest cases pass under wasmtime/node with the Swift overlay enabled (swift.org 6.3.3 toolchain + Swift 6.3.3 Wasm SDK). Co-authored-by: Krzysztof Rodak <krodak.konta@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add file-descriptor readiness and in-process signal delivery to the WASI event backend, closing the two API families the port previously rejected that WASI can actually express. Read/write sources ride preview1 poll_oneoff fd subscriptions through wasi-libc poll(2) — deliberately the POSIX surface rather than raw __wasi_* calls, so the same code carries unchanged to wasip2, where wasi-libc maps poll() onto wasi:io/poll and adds in-guest socket creation. A muxnote layer mirroring the epoll backend (readers/writers lists, EV_DISPATCH disarm/rearm, hangup delivery) merges readiness into the cooperative drain at every wait point: dispatch_main(), blocking semaphore/group/block waits, and timed waits all wake on fd events, and a blocking wait whose progress can only come from an armed fd source parks in the host poll instead of crashing as a deadlock. Signal sources ride wasi-libc's _WASI_EMULATED_SIGNAL (already defined and linked by this build): registration installs the backend's handler, an in-process raise() records the delivery synchronously, and the next harvest merges the accumulated count. No WASI version has asynchronous or cross-process signals, so this is the full expressible semantic; the code compiles out (and registration crashes loudly) without the emulation macro. Guardrails, each validated against a specific runtime defect: - regular files/directories are never polled (always-ready merge, like epoll's EPERM path): Node's uvwasi errors on fd subscriptions for regular files, and POSIX calls them always ready anyway - a capability probe at registration crashes with a named diagnostic on hosts whose poll_oneoff lacks fd subscriptions (browser_wasi_shim, uwasi) instead of hanging later; invalid fds crash at registration, fds closed while armed crash at the next wait - indefinite waits poll in bounded 1-hour slices: wasi-libc encodes timeout -1 as a subscription set with no clock entry, which WasmKit's host mishandles (trap) - with no fd source armed, idle waits remain a single-clock nanosleep, preserving nanosecond timer precision and single-subscription-shim compatibility - merges during the poll harvest only record pending work instead of eagerly draining: a handler running mid-harvest could cancel a source and free muxnotes the harvest is still iterating Runtime matrix (verified): wasmtime, Node node:wasi, and WasmKit run the full fd + signal test set; browser_wasi_shim runs signal sources (pure libc) and fails fd sources loudly at registration; uwasi has no poll_oneoff at all. Tests: signal-source.c becomes a functional test (count accumulation); unsupported-source.c now checks the invalid-fd crash; new write-source.c (readiness + level-triggered rearm + dispatch_main parking), read-source.c and fd-wakeup-wait.c (the runner's new --stdin-after pipes data only after a delay, proving the guest parks in the host poll and wakes on readiness, once under dispatch_main and once inside a blocking semaphore wait). 38/38 ctest cases pass with ENABLE_SWIFT=YES. Co-authored-by: Krzysztof Rodak <krodak.konta@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comparison: PR #1 vs PR #2 vs PR #3 (libdispatch → Wasm/WASI)(Updated after two review rounds on #3: a full-branch review with fixes, then an adversarial probe round from the #2 side with a hardening series. Every row is verified empirically — built and executed. Cross-posted on #1, #2, and #3.) All three branches were built for Branch summary
Dispatch API supportLegend: ✅ works · Queues & submission
Synchronization
Sources
Data & I/O
Test coverage
Harness note (#3): Node is now optional — without it (or below 19.8) everything still builds and all tests register as visible-but-DISABLED, the same shape as the missing- Runtime support
Review round (what changed since the previous version of this comment)Two rounds. First, a high-effort review of #3's full diff vs Second, an adversarial probe round from the #2 side found one bypassed funnel in the poke-defer fix ( Bottom line#1 is the minimal seed but breaks contracts on APIs it nominally supports. #2 made the supported set behave per spec and the unsupported set fail loudly; the review round found its one systemic gap — eager drains running client code beneath caller-held locks — plus a silent-spin path and the signal/cap issues, all inherited by and now fixed in #3. #3 is #2's design carried to completion: fd and signal sources work, the re-entrancy divergence from threaded platforms is confined to documented, pinned-by-test semantics, and every failure mode is a named crash. The remaining unsupported surface (process/vnode/memory-pressure/Mach, cross-process signals, true parallelism) is bounded by WASI itself. 🤖 Generated with Claude Code |
…e headers Review-readiness items from the two-ports handoff report: - eager-drain.c pins the port's one deliberate divergence from threaded Dispatch as specified behavior: a top-level dispatch_async runs the block on the submitting stack before returning, nested submissions defer to the outer drain and resume in category priority order (main queue before root queues regardless of submission order), and a group emptied at submit runs its notify before later submissions. These orderings were previously observable but unpinned; changing them is now a test failure, not an accident. - tests/wasm no longer hard-requires Node at configure time. When Node is missing or older than 19.8, everything still builds and every test stays registered in ctest as DISABLED — the same visible-but-disabled shape the upstream-subset tests use when WASI_TEST_RUNNER is absent. Verified by configuring and building with a PATH containing no node: 101 targets build, 37 tests register disabled. - Apache license headers on all WASI test sources that lacked them. - README: document eager submission as specified behavior (and its re-entrancy cost), the wall-timer anchor-at-arm decision as a deliberate WASI limitation (no clock-change notification exists), and the wasip1-threads boundary (the #error guard is the seam where a future threaded port forks to the normal worker-pool model). 39/39 ctest cases pass with ENABLE_SWIFT=YES. Co-authored-by: Krzysztof Rodak <krodak.konta@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…structor push under dqsh_lock Pumping-wait call-site audit results (tests/wasm/WAIT-PUMPING-AUDIT.md): - All six wait-primitive call sites are classified: the public semaphore/group waits and the contended-sync park pump by design; the dispatch_apply waiter never blocks single-threaded; the source-cancel waiter converges via the manager-queue drain. Nested waits never pump. - _dispatch_thread_switch on WASI was inherited as an empty body, so a contended unfair lock or once gate would spin hot forever. On the sole thread that state is always a dead one (recursion is caught earlier by the owner check); it now crashes with a named diagnostic, closing the last hang-shaped behavior in the port. - One real hazard found and fixed: dispatch_queue_set_specific submitted the replaced value's destructor to a root queue while dqsh_lock was held. Under the eager drain the push can run the client destructor immediately on the same stack, under the lock; a destructor touching the same queue's specifics would deadlock. The push now happens after unlock (destructor submissions carry no ordering guarantee, so this is unobservable on threaded platforms). specific-destructor.c pins it: the destructor re-reads the same queue's specifics. Also verified this session: exact CMake 3.31.0 configures, builds, and passes 37/37 (C-only); Linux native (clang/ninja, aarch64) builds main and this branch identically at 23/23 upstream tests each. 40/40 WASI ctest cases pass with ENABLE_SWIFT=YES. Co-authored-by: Krzysztof Rodak <krodak.konta@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review-driven fixes, each with a test written first:
- Eager drains no longer run client code beneath critical sections
entered outside a drain. A new poke-defer bracket
(_dispatch_wasi_defer_pokes/_undefer_pokes, no-ops elsewhere) wraps
the inline sync/async_and_wait funnels, dispatch_once initializers,
object dispose, and the dispatch_queue_set_specific critical section:
pokes inside only record pending work, and the outermost undefer
flushes it. dispatch_sync(qA){dispatch_async(qB){dispatch_sync(qA)}}
— correct on every threaded platform — previously crashed 'queue
already owned by current thread'; sync-nested-async.c pins the fix.
The three eager-drain guards also collapse into one named choke
point (_dispatch_wasi_drain_unless_deferred).
- dispatch_queue_set_specific reverts to the upstream shape (the
destructor push under dqsh_lock), now safe under the bracket: the
earlier reordering changed submission-visibility semantics on
threaded platforms and is no longer needed.
- The poll set grows geometrically instead of trapping at 64 armed fd
sources (a load-dependent crash at an arbitrary wait point).
- Signal sources save the application's signal() disposition at
registration and restore it at unregistration (previously reset to
SIG_DFL, turning the app's next raise() into termination), and check
signal() for SIG_ERR. signal-disposition.c pins it. The pending-
signal latch is now consistent: stale counts are consumed even with
no live muxnote, and unregistration recomputes the aggregate flag.
- run-wasi-test.mjs cancels the --stdin-after timer when the guest
exits first, guards the write, and ignores EPIPE from a dying guest;
a runner-robustness test covers the early-exit case.
- The WASI main-queue drain is no longer a 63-line fork: the
DISPATCH_COCOA_COMPAT _dispatch_main_queue_drain is hoisted to a
shared definition whose two runloop/QoS steps compile only for
COCOA_COMPAT, and the WASI entry point is a two-line wrapper.
Review findings NOT fixed, on evidence: the timed-semaphore-wait
'missed final consume' is compensated upstream (_dispatch_semaphore_
wait_slow re-checks dsema_value on timeout — sema-late-signal.c proves
the signal is delivered, not stranded), and close-while-armed keeps its
named crash (epoll's actual behavior is silent-never-fires; kqueue logs
a vanish; the crash is the most debuggable of the three).
44/44 WASI ctest cases pass; Linux native build unchanged at 23/23.
Co-authored-by: Krzysztof Rodak <krodak.konta@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Bracket _dispatch_barrier_trysync_or_async_f's inline invoke with the poke-defer pair: it runs internal-state mutation under the acquired barrier lock, and a captured value's release there could poke. - ASCII-only punctuation in all added comments and docs; drop pull-request cross-references from test file comments; remove a duplicated wall-timer paragraph from the tests/wasm README. 44/44 WASI ctest cases pass. Co-authored-by: Krzysztof Rodak <krodak.konta@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The shared main-queue drain's crash diagnostics name the platform's
actual entry point (_dispatch_wasi_main_queue_drain on WASI,
_dispatch_main_queue_callback_4CF elsewhere); the Darwin strings are
unchanged.
- WASI.cmake no longer requires the Swift static-resource and Swift
clang-resource directories for ENABLE_SWIFT=OFF builds; C-only
consumers need only the toolchain, the sysroot, and the builtins
archive.
- The cooperative wait policy lives in one helper
(_dispatch_wasi_wait_park); the three wait loops keep their exact
per-primitive crash diagnostics (DISPATCH_CLIENT_CRASH requires
literal messages) and now share one ordering.
- The runner gains --deny-fd-poll, emulating a host whose poll_oneoff
rejects fd subscriptions; dispatch_wasi_fd_poll_denied pins the
previously untestable capability crash ('this WASI runtime does not
support file-descriptor readiness').
- New dispatch_wasi_io_stream test: a DispatchIO stream channel on
stdin completes a read whose payload arrives 250ms after the wait
begins, closing the last untested capability claim (DispatchIO over
non-regular descriptors).
46/46 WASI ctest cases pass.
Co-authored-by: Krzysztof Rodak <krodak.konta@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_dispatch_async_and_wait_block_with_privdata (the entry for dispatch_async_and_wait / dispatch_barrier_async_and_wait with a dispatch_block_create() block - what Swift's DispatchQueue.asyncAndWait(execute: DispatchWorkItem) produces) called _dispatch_async_and_wait_recurse directly, bypassing the poke-defer bracket every other sync/async_and_wait funnel got. A mere dispatch_async inside such a block eager-drained within the _dispatch_fake_wlh ANON region and died with the internal-bug crash 'Lingering DISPATCH_WLH_ANON' (reproduced on wasmtime and WasmKit; the plain-block funnel was bracketed and fine). Fix is the same two lines _dispatch_async_and_wait_f uses; the return-of-void to bare-call change is codegen-identical and the macros are ((void)0) off WASI, so non-WASI builds are unchanged. Swept the remaining recurse/wait funnels: _dispatch_async_and_wait_ recurse now has only bracketed callers, and _dispatch_sync_recurse is reachable only through the bracketed inline wrappers. async-and-wait.c pins the fix (test written first, failed with the ANON crash): plain-block control with a nested async->sync-back, plus privdata blocks in benign-async, full-nesting, and barrier shapes, asserting completion before each return. 47/47 WASI ctest cases pass. Co-authored-by: Scott Marchant <15382220+scottmarchant@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Pushed What was bypassed: The crash: a The fix: the same defer/undefer pair New test: Fresh |
Review summary from the #2 sideReproduced the headline (46/46, now 47/47 with the 1. "Never beneath caller-held locks" is only true for pokesBlocking waits still pump other queues' work under the caller's lock: 2. Event sources need a blocking wait or
|
A read source on a pipe whose peer closes becomes permanently ready on hosts that never set poll_oneoff's FD_READWRITE_HANGUP flag (wasmtime 47 for pipes, empirically; preview1 offers no other EOF signal - nbytes is a constant 1 and fd_filestat_get gives no pipe fill). The readable-at-EOF fire matches Darwin, where the handler observes read() == 0 and must cancel - but a client that never cancels turned a parked dispatch_main or blocking wait into a silent hot loop (measured: 831k handler fires in 2s on wasmtime, 268k on Node with the hangup flag suppressed), against the port's no-silent-spin policy. Hosts that do report hangup were and remain bounded: the POLLHUP harvest path delivers EOF and stops watching the descriptor, the epoll backend's EPOLLHUP discipline. The guard is a windowed rate check at the harvest poll: readiness reported 100000 times within two seconds of parked polling means the polls return instantly (under 20us average), which a demand-driven stream cannot sustain; that crashes with a named diagnostic pointing at the un-canceled source. A per-poll elapsed cutoff was tried first and is not viable: host scheduling jitter (Node's event loop pauses every few hundred polls) resets any consecutive counter indefinitely. All changes are inside the DISPATCH_EVENT_BACKEND_WASI guard; non-WASI builds are unchanged. pipe-eof-source.c pins both host shapes (test written first, spun to the 10s watchdog unfixed): the natural Node hangup path delivers EOF then traps idle, and under the runner's new --suppress-poll-hangup flag (which emulates the wasmtime host by masking the hangup flag out of poll_oneoff results) the same binary must produce the named spin crash, after observing the 0-byte read. 49/49 WASI ctest cases pass. Co-authored-by: Scott Marchant <15382220+scottmarchant@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The advertised 'file descriptor closed while dispatch source is armed' diagnostic was reachable only through the harvest's POLLNVAL branch, which wasi-libc feeds from a per-subscription BADF error - but wasmtime reports a closed armed fd by failing the whole poll_oneoff call with BADF instead, so consumers there got the generic 'poll() failed for armed dispatch source file descriptors (cause: 0x8)'. Since every descriptor in the harvest set belongs to an armed source, a whole-call EBADF can only mean one of them was closed while armed: that errno now produces the same named diagnostic, and the POLLNVAL branch stays for hosts that report the condition per subscription (with a comment citing the wasi-libc mapping). WASI-only code; non-WASI builds are unchanged. fd-closed-while-armed pins the EBADF shape (test written first, saw the generic message) through the runner's new --fd-poll-ebadf-after flag, which lets the registration capability probe through and fails every later fd poll with BADF, and a real close(0) under wasmtime produces the named message as well. README documents the Node caveat: the guest closing an armed stdio descriptor aborts the host inside libuv before libdispatch can see anything. 50/50 WASI ctest cases pass. Co-authored-by: Scott Marchant <15382220+scottmarchant@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Registration crashes on SIG_ERR from signal(); the unregistration path restoring the application's previous disposition ignored it. The signal number was already validated when registration installed the handler, so a failing restore indicates library/host inconsistency rather than client misuse - an internal crash, mirroring the registration check. Purely defensive: wasi-libc's emulated signal() cannot fail for a signo it previously accepted, which is also why the branch has no test. signal-source and signal-disposition still pass. Co-authored-by: Scott Marchant <15382220+scottmarchant@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two claimed-verified behaviors had no in-tree test: - group-fd-wakeup.c: a blocking dispatch_group_wait(FOREVER) satisfied by fd readiness, the read source's handler leaving the group (the existing blocking-waits.c group wait never parks - its async empties the group at submit - and fd-wakeup-wait.c covers only semaphores). - regfile-io.c: dispatch_read/dispatch_write/DispatchIO on a regular file through the always-ready path, with payload assertions: a whole file write and content-checked read-back plus a DISPATCH_IO_RANDOM byte-range read. The runner grows a --preopen <guest>:<host> flag to map a scratch directory into the guest. Both verified failing when broken (missing preopen, wrong payload) and green under the Node runner and wasmtime. 52/52 WASI ctest cases pass. Co-authored-by: Scott Marchant <15382220+scottmarchant@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- README: dispatch_main()'s idle trap is qualified - an armed signal source alone cannot keep the park alive, since signals are in-process raise() only and a parked sole thread with nothing runnable could never be signaled; a signal-source-only dispatch_main() traps as truly idle by design. - README: state the two boundary contracts explicitly - pokes defer inside caller-held critical sections but blocking waits issued there still pump under the caller's lock, and fd/signal sources deliver only at blocking waits or inside dispatch_main() (eager drains fire timers, never harvest sources). - README: signal delivery happens at the next blocking wait or park, not 'the next drain'; same fix for the pending-signal latch comment in event_wasi.c. - WAIT-PUMPING-AUDIT: the set_specific fix description now matches the shipped shape (destructor push under dqsh_lock inside the poke-defer bracket, flush deferred past unlock) instead of the earlier capture-then-push-after-unlock draft; summary notes the ready-poll rate guard closing the permanently-ready spin case. - README: neutral wording for the tests adapted during the port's earlier iterations. Comment-only src change; 52/52 WASI ctest cases pass. Co-authored-by: Scott Marchant <15382220+scottmarchant@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
H9 follow-ups landed on the branch (
New tests: |
9e1a5ac to
41678b4
Compare
41678b4 to
b93a792
Compare
Overview
This PR is the complete WASI port of libdispatch. It makes
import Dispatchwork onwasm32-wasip1, which runs on one thread with no way to block. The port contains the C runtime, the Swift overlay, file-descriptor and signal event sources, and a 52-test suite that includes 13 unmodified upstream tests.This PR stays open as the single view of the full change set against
main. We will not merge it as one unit. We will submit the work upstream as five small, independent PRs. Each upstream PR gets an internal draft in this fork first.dispatch_mainos(WASI)gates, consumer link testsOrder: A lands first and alone. B1 lands next. B2, C, and D land in any order after B1. Fork-only material (runner self-checks, audit prose, extended README semantics) stays out of the upstream slices.
Threads research
The branch
feat/scottm/libDispatchWasmThreads(four commits off this PR's tip) answers the multi-threading question. libdispatch now compiles forwasm32-unknown-wasip1-threadsbehind an experimentalDISPATCH_WASI_THREADS=ONCMake knob. In that mode it behaves like a generic POSIX platform: a pthread worker pool, real blocking waits on a wasm futex, POSIX semaphores, and a manager thread on a condition variable. The proof runs under wasmtime v24 (-S threads) and printsPASS: async=8/8 on-worker-thread=8/8 sync=1 timer=1: everydispatch_asyncblock ran on a worker thread, semaphores blocked and woke across threads,dispatch_syncfunneled, anddispatch_afterfired.Two results matter for this series. First, the slice A seams need no change and no API signature moves: the poke-defer hooks compile to no-ops in threads mode, exactly as on every threaded platform. Second, the cooperative backend stays the default: current wasmtime has removed wasi-threads support (v24 LTS still runs it), so threads mode ships as an experiment. Not implemented there yet: fd and signal sources,
dispatch_main(), and the Swift overlay. Full findings, the gate audit, and reproduction commands live intests/wasm/THREADS-RESEARCH.mdon that branch.How the port works
libdispatch is mostly data structures: queues, a state machine per queue, a timer heap, and unified event sources. Threads only drive that machinery. The port keeps the machinery and replaces the driver with a cooperative drain on the only thread there is.
dispatch_asyncenqueues through normal upstream code. The poke that would wake a worker instead drains pending work on the spot, unless a drain is already running.dispatch_onceinitializer, an object dispose) defer until the section exits. Blocking waits issued inside such sections still pump; that is required for progress and is documented as the contract.poll_oneoffthrough wasi-libcpoll(2). Signal sources cover in-processraise(). Sources deliver during blocking waits anddispatch_main().How we validated it
mainand this branch pass the identical 23 of 23 upstream tests.History and key decisions
Two review rounds ran against the full diff, one from each author's side. Details live in the comments on this PR. Key decisions on record:
dispatch_asyncruns the block before it returns. This is what makes the library work in hosts that never calldispatch_main()._dispatch_semaphore_wait_slowre-checks the semaphore value on timeout and delivers the raced signal. The reproduction test stays in the suite as a pin.Remaining work
A - shared-code preparation (#5)
B1 - build system and core runtime
dispatch_once)B2 - test suite
bsdtestsmacros where they are not alreadyctestuntil a WASI lane existsC - Swift overlay
D - event sources
Cross-slice
mainResearch and ecosystem
wasm32-wasip1-threads). PROVEN on branchfeat/scottm/libDispatchWasmThreads: libdispatch builds with-DDISPATCH_WASI_THREADS=ONand the smoke test reportsPASS: async=8/8 on-worker-thread=8/8 sync=1 timer=1under wasmtime v24. The slice A seams need no change and no API signature moves, so Prepare shared code for cooperative single-threaded event backends #5 is unblocked. Details:tests/wasm/THREADS-RESEARCH.mdon that branch.#if canImport(Dispatch)is mis-used: find the WASI code paths that become wrong, or break, when real Dispatch support for wasm rolls out