Skip to content

Box JSException storage in a class to fit the direct typed-error convention#13

Draft
krodak wants to merge 1 commit into
kr/closures-throws-asyncfrom
kr/box-jsexception-storage
Draft

Box JSException storage in a class to fit the direct typed-error convention#13
krodak wants to merge 1 commit into
kr/closures-throws-asyncfrom
kr/box-jsexception-storage

Conversation

@krodak

@krodak krodak commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Overview

An intermediate solution for the async closure reject path broken by swiftlang/swift#89320, until the IRGen fix in swiftlang/swift#89715 ships.

JSException (~36 bytes) exceeds the direct typed-error convention on wasm32, so an async throws(JSException) closure value takes the indirect-error path that swiftlang/swift#89320 miscompiles: when the closure is captureless, the thrown error is corrupted across the async unwind and the rejected Promise receives garbage instead of the JSException. Boxing JSException's storage into a single class reference (~4 bytes) moves it into the direct convention, so the broken path is never taken.

The function-export variant of the same bug was fixed separately at the codegen level in swiftwasm#760 (already on main); this PR addresses the closure side, which codegen cannot reach (the closure value is constructed by user code).

1. Boxing, conditional on compiler(>=6.3). thrownValue, description, and stack move into a private final class; the struct holds one stored reference. The public surface is unchanged (computed accessors, same initializers, equivalent Equatable); the cost is one heap allocation per thrown exception, on the error path only. The boxed layout crashes the Swift 6.1 wasm IRGen (Invalid InsertValueInst operands), so older compilers keep the original stored properties and today's behavior. 6.2 is unverified and conservatively kept on the legacy layout; the threshold can be lowered after verification.

2. Reject tests gated by capability, not blanket. A @JS probe (asyncThrowsClosureRejectSupported) reports whether the boxed layout is active; the Swift-to-JS async throws(JSException) closure reject assertions run whenever it is. On Swift 6.3 and later they run and pass; on 6.1 they are skipped (status quo there). The blanket ASYNC_THROWS_CLOSURE_REJECT_BLOCKED gate from the base PR is removed.

3. Base-PR mitigations removed. With boxing active on current toolchains, the build-time warning and most doc callouts from the base PR are dropped; the support table notes the reject path requires Swift 6.3 or later.

4. JSClosure.async reject path. The same bug shape existed in JSClosure.async and was untested; this PR adds the missing reject-path test (gated on compiler(>=6.3)), which passes with boxing.

Notes

@krodak krodak force-pushed the kr/closures-throws-async branch from 30a9c43 to aa02b44 Compare June 10, 2026 18:10
@krodak krodak force-pushed the kr/box-jsexception-storage branch from 1744fd4 to 583c12b Compare June 10, 2026 18:10
@krodak krodak force-pushed the kr/closures-throws-async branch from aa02b44 to 982e9fb Compare June 10, 2026 18:30
@krodak krodak force-pushed the kr/box-jsexception-storage branch from 583c12b to da5ff9c Compare June 10, 2026 18:30
@krodak krodak force-pushed the kr/box-jsexception-storage branch from da5ff9c to 89de6cd Compare June 10, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant