BridgeJS: Fix reject path of zero-parameter async throwing exports by krodak · Pull Request #760 · swiftwasm/JavaScriptKit
Overview
A zero-parameter async throws(JSException) export traps the Wasm instance when it throws. The generated thunk wraps the call in a _bjs_makePromise body closure; with no parameters there is nothing to capture, so the closure lowers via thin_to_thick_function, and invoking that value with an indirect typed error miscompiles on Wasm (swiftlang/swift#89320): the thrown JSException arrives corrupted in Promise_reject and lowering it traps with RuntimeError: table index is out of bounds (or the promise rejects with a garbage value). Exports with parameters are unaffected because their hoisted parameter locals are captured, which makes the body closure a partial apply with a matching call signature.
@JS func ping() async throws(JSException) -> String { throw JSException(JSError(message: "failure").jsValue) }
Generated thunk before:
return _bjs_makePromise(resolve: Promise_resolve_SS, reject: Promise_reject) { () async throws(JSException) -> String in return try await ping() }
After:
let __bjs_capture = 0 return _bjs_makePromise(resolve: Promise_resolve_SS, reject: Promise_reject) { [__bjs_capture] () async throws(JSException) -> String in _ = __bjs_capture return try await ping() }
1. Force a capture in captureless async throwing thunk bodies. When the body closure would otherwise capture nothing (zero-parameter top-level functions, static methods, and constructors share this shape), the generator emits a dummy local captured by the closure. The body must also read the captured value: an unread capture list entry is dropped by capture analysis, leaving the closure thin and the bug in place. The read is what produces a real partial_apply.
2. Tests. A codegen snapshot covers the zero-parameter shape, and an end-to-end regression test asserts the rejection arrives with the thrown message instead of trapping. Existing thunks are emitted byte-identically.
This is a codegen-level workaround for swiftlang/swift#89320; once the IRGen fix in swiftlang/swift#89715 lands in a release toolchain, the forced capture can be removed.