Skip to content

BridgeJS: Support throws and async for closures#11

Draft
krodak wants to merge 1 commit into
mainfrom
kr/closures-throws-async
Draft

BridgeJS: Support throws and async for closures#11
krodak wants to merge 1 commit into
mainfrom
kr/closures-throws-async

Conversation

@krodak

@krodak krodak commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Overview

Adds throws(JSException) and async (and async throws(JSException)) support to BridgeJS-bridged closures, in both directions: closures Swift receives from JavaScript, and closures Swift hands to JavaScript.

Async settlement reuses the _bjs_makePromise + per-type Promise_resolve_<mangled> / Promise_reject machinery introduced in swiftwasm#758, so every bridged return type (@JS struct, enums, Optional/Array/Dictionary) is inherited for free; no parallel mechanism is introduced.

1. Effect-aware closure mangling. Closure ABI symbol names encode effects with the Swift ABI operators (Ya for async, K for throws), so signatures that differ only by effects no longer collide.

2. Throwing closures (both directions). A JavaScript callback's thrown JSException propagates into Swift; a Swift closure's thrown JSException is routed back to JavaScript. Only throws(JSException) is supported; plain throws is diagnosed.

3. Async closures (both directions). A JS-to-Swift callback is awaited via _bjs_awaitPromise; a Swift-to-JS closure returns a Promise settled via _bjs_makePromise. TypeScript surfaces these as (args) => Promise<R>. Unsupported async return types (associated-value enums, protocols, namespace enums, including nested in Optional/Array/Dictionary) are diagnosed rather than miscompiled.

4. Lifetime. The closure box survives across suspension without an explicit pin: the extracted closure value, captured by the settling Task, keeps it alive even if JavaScript releases the closure mid-flight. Covered by release-race and concurrent-invocation tests.

Known limitation

A Swift-to-JS async throws(JSException) closure resolves correctly, but a thrown error does not currently propagate to JavaScript on the reject path. This is a Swift compiler bug on wasm32, swiftlang/swift#89320, with a fix in progress in swiftlang/swift#89715: when the typed error is too large for the direct error convention (as JSException is) and the closure value is captureless (or a function reference), IRGen and the wasm calling-convention padding disagree on parameter order at the thick call site, and the thrown error is lost across the async unwind.

The resolve path is unaffected, capturing closures are unaffected (a practical workaround), and JS-to-Swift async-throwing callbacks are unaffected. BridgeJS now emits a build-time warning when it sees the at-risk signature (a Swift-to-JS async throws(JSException) closure), pointing at swiftlang/swift#89320, so users are aware before they hit it at runtime. The reject assertions are gated with a pointer to swiftlang/swift#89320 and the limitation is documented in the closure articles. A stacked follow-up PR provides a standalone reproducer demonstrating the bug in isolation.

Tests

Codegen snapshots, the cross-swift-syntax-version matrix, and end-to-end runtime round-trips (both directions, throwing and async, resolve and reject, Void, @JS struct returns, the release-race, and concurrent calls) pass. The generated error.description lowering is consistent with swiftwasm#759.

@krodak krodak force-pushed the kr/closures-throws-async branch 2 times, most recently from 746c781 to 09bb61c Compare June 10, 2026 09:45
@krodak krodak force-pushed the kr/closures-throws-async branch from 09bb61c to 30a9c43 Compare June 10, 2026 10:34
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