◐ Shell
clean mode source ↗

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.