@@ -16,7 +16,8 @@ public struct ClosureCodegen {
1616 let closureParams = signature. parameters. map { " \( sendingPrefix) \( $0. closureSwiftType) " } . joined (
1717 separator: " , "
1818 )
19- let swiftEffects = ( signature. isAsync ? " async " : " " ) + ( signature. isThrows ? " throws " : " " )
19+ // BridgeJS only bridges `throws(JSException)` closures, so emit the typed throws clause.
20+ let swiftEffects = ( signature. isAsync ? " async " : " " ) + ( signature. isThrows ? " throws(JSException) " : " " )
2021 let swiftReturnType = signature. returnType. closureSwiftType
2122 return " ( \( closureParams) ) \( swiftEffects) -> \( swiftReturnType) "
2223 }
@@ -72,8 +73,24 @@ public struct ClosureCodegen {
7273 helperEnumDeclPrinter. write ( " static func bridgeJSLift(_ callbackId: Int32) -> \( swiftClosureType) { " )
7374 helperEnumDeclPrinter. indent {
7475 helperEnumDeclPrinter. write ( " let callback = JSObject.bridgeJSLiftParameter(callbackId) " )
76+ // Throwing/async closures need an explicit signature with a typed effect
77+ // clause: a bare multi-statement closure literal infers its thrown type as
78+ // `any Error` even when assigned to a `throws(JSException)` function type.
79+ // Spelling out parameter types, the effect clause, and return type pins
80+ // inference to `JSException`.
7581 let parameters : String
76- if signature. parameters. isEmpty {
82+ if signature. isThrows || signature. isAsync {
83+ let sendingPrefix = signature. sendingParameters ? " sending " : " "
84+ let typedParams =
85+ signature. parameters. enumerated ( ) . map { index, paramType in
86+ " param \( index) : \( sendingPrefix) \( paramType. closureSwiftType) "
87+ } . joined ( separator: " , " )
88+ let returnType = signature. returnType. closureSwiftType
89+ // Canonical effect order is `async` before `throws` (Mangling.rst).
90+ let effects =
91+ ( signature. isAsync ? " async " : " " ) + ( signature. isThrows ? " throws(JSException) " : " " )
92+ parameters = " ( \( typedParams) ) \( effects) -> \( returnType) "
93+ } else if signature. parameters. isEmpty {
7794 parameters = " "
7895 } else if signature. parameters. count == 1 {
7996 parameters = " param0 "
@@ -146,22 +163,29 @@ public struct ClosureCodegen {
146163 liftedParams. append ( " \( paramType. swiftType) .bridgeJSLiftParameter( \( argNames. joined ( separator: " , " ) ) ) " )
147164 }
148165
149- let closureCallExpr = ExprSyntax ( " closure( \( raw: liftedParams. joined ( separator: " , " ) ) ) " )
166+ let tryPrefix = signature. isThrows ? " try " : " "
167+ let closureCallExpr = ExprSyntax ( " \( raw: tryPrefix) closure( \( raw: liftedParams. joined ( separator: " , " ) ) ) " )
168+ let asyncTryPrefix = ( signature. isThrows ? " try " : " " ) + " await "
169+ let asyncClosureCallExpr = ExprSyntax (
170+ " \( raw: asyncTryPrefix) closure( \( raw: liftedParams. joined ( separator: " , " ) ) ) "
171+ )
150172
151- let abiReturnWasmType = try signature. returnType. loweringReturnInfo ( ) . returnType
173+ // An async closure returns a JS `Promise` (a `JSObject`) synchronously and settles
174+ // it later, like an exported `async` Swift function, so the ABI return is always
175+ // the Promise object id (`i32`) regardless of the closure's logical return type.
176+ let abiReturnWasmType =
177+ signature. isAsync
178+ ? try BridgeType . jsObject ( nil ) . loweringReturnInfo ( ) . returnType
179+ : try signature. returnType. loweringReturnInfo ( ) . returnType
152180
153181 // Build signature using SwiftSignatureBuilder
154182 let funcSignature = SwiftSignatureBuilder . buildABIFunctionSignature (
155183 abiParameters: abiParams,
156184 returnType: abiReturnWasmType
157185 )
158186
159- // Build function declaration using helper
160- let funcDecl = SwiftCodePattern . buildExposedFunctionDecl (
161- abiName: abiName,
162- signature: funcSignature
163- ) { printer in
164- printer. write ( " let closure = Unmanaged< \( boxType) >.fromOpaque(boxPtr).takeUnretainedValue().closure " )
187+ // Emits the closure call and lowers its return value onto the ABI.
188+ let emitCallAndLower : ( CodeFragmentPrinter ) -> Void = { printer in
165189 if signature. returnType == . void {
166190 printer. write ( closureCallExpr. description)
167191 } else {
@@ -189,6 +213,101 @@ public struct ClosureCodegen {
189213 }
190214 }
191215
216+ // Emits the async closure call wrapped in a `_bjs_makePromise` and lowers the
217+ // resulting Promise object onto the ABI, reusing the mechanism the exported
218+ // async-function thunk uses: `_bjs_makePromise(resolve: Promise_resolve_<mangleR>,
219+ // reject: Promise_reject)` with a `() async throws(JSException) -> R in` body. The
220+ // closure's return type R is collected into the shared
221+ // `asyncPromiseResolveReturnTypes` set, so its concrete `Promise_resolve_<R>` thunk
222+ // is emitted alongside the function ones.
223+ //
224+ // No explicit box pin is needed across suspension: `takeUnretainedValue().closure`
225+ // copies the closure value out of the box, and that copy is captured by the
226+ // escaping `_bjs_makePromise` body, keeping it alive for the whole settling `Task`
227+ // independent of the box's JS-owned refcount.
228+ let emitAsyncCallAndLower : ( CodeFragmentPrinter ) -> Void = { printer in
229+ printer. write ( " let closure = Unmanaged< \( boxType) >.fromOpaque(boxPtr).takeUnretainedValue().closure " )
230+ let resolveType = signature. returnType
231+ let resolveName = " Promise_resolve_ \( resolveType. mangleTypeName) "
232+ let rejectName = " Promise_reject "
233+ // A throwing async body needs an explicit `() async throws(JSException) -> R`
234+ // head, otherwise Swift infers `throws(any Error)` for a multi-statement
235+ // closure literal (see https://github.com/swiftlang/swift/issues/76165).
236+ let closureHead : String
237+ if signature. isThrows {
238+ let returnSpelling = resolveType == . void ? " " : " -> \( resolveType. closureSwiftType) "
239+ closureHead = " () async throws(JSException) \( returnSpelling) in "
240+ } else {
241+ closureHead = " "
242+ }
243+ printer. write ( " return _bjs_makePromise(resolve: \( resolveName) , reject: \( rejectName) ) { \( closureHead) " )
244+ printer. indent {
245+ if resolveType == . void {
246+ printer. write ( asyncClosureCallExpr. description)
247+ } else {
248+ printer. write ( " return \( asyncClosureCallExpr) " )
249+ }
250+ }
251+ printer. write ( " } " )
252+ }
253+
254+ // The placeholder return statement used on the catch path so the exposed
255+ // function still satisfies its ABI return type after routing the thrown
256+ // JSException to JS via `_swift_js_throw`.
257+ let catchPlaceholderStmt = abiReturnWasmType? . swiftReturnPlaceholderStmt
258+
259+ // Build function declaration using helper
260+ let funcDecl = SwiftCodePattern . buildExposedFunctionDecl (
261+ abiName: abiName,
262+ signature: funcSignature
263+ ) { printer in
264+ if signature. isAsync {
265+ // `emitAsyncCallAndLower` extracts `closure` from the box itself, so the
266+ // unconditional unretained extraction below is skipped for the async path.
267+ emitAsyncCallAndLower ( printer)
268+ } else if signature. isThrows {
269+ printer. write (
270+ " let closure = Unmanaged< \( boxType) >.fromOpaque(boxPtr).takeUnretainedValue().closure "
271+ )
272+ // Route the caught JSException across the ABI like a throwing exported
273+ // function; the JS wrapper checks `tmpRetException` and rethrows.
274+ printer. write ( " do { " )
275+ printer. indent {
276+ emitCallAndLower ( printer)
277+ }
278+ printer. write ( " } catch let error { " )
279+ printer. indent {
280+ printer. write ( " if let error = error.thrownValue.object { " )
281+ printer. indent {
282+ printer. write ( " withExtendedLifetime(error) { " )
283+ printer. indent {
284+ printer. write ( " _swift_js_throw(Int32(bitPattern: $0.id)) " )
285+ }
286+ printer. write ( " } " )
287+ }
288+ printer. write ( " } else { " )
289+ printer. indent {
290+ printer. write ( " let jsError = JSError(message: error.description) " )
291+ printer. write ( " withExtendedLifetime(jsError.jsObject) { " )
292+ printer. indent {
293+ printer. write ( " _swift_js_throw(Int32(bitPattern: $0.id)) " )
294+ }
295+ printer. write ( " } " )
296+ }
297+ printer. write ( " } " )
298+ if let catchPlaceholderStmt {
299+ printer. write ( catchPlaceholderStmt)
300+ }
301+ }
302+ printer. write ( " } " )
303+ } else {
304+ printer. write (
305+ " let closure = Unmanaged< \( boxType) >.fromOpaque(boxPtr).takeUnretainedValue().closure "
306+ )
307+ emitCallAndLower ( printer)
308+ }
309+ }
310+
192311 return DeclSyntax ( funcDecl)
193312 }
194313
@@ -200,6 +319,9 @@ public struct ClosureCodegen {
200319 guard !signatureAccessLevels. isEmpty else { return nil }
201320
202321 var decls : [ DeclSyntax ] = [ ]
322+ // Async closures settle their JS `Promise` through the shared `_bjs_makePromise`
323+ // + per-type `Promise_resolve_<mangleR>` / `Promise_reject` mechanism emitted by
324+ // `ExportSwift`, so no closure-specific settlement helpers are needed here.
203325 for signature in signatureAccessLevels. keys. sorted ( by: { $0. mangleName < $1. mangleName } ) {
204326 let accessLevel = signatureAccessLevels [ signature] ?? . internal
205327 decls. append ( contentsOf: try renderClosureHelpers ( signature, accessLevel: accessLevel) )
0 commit comments