@@ -90,11 +90,75 @@ public class ExportSwift {
9090 decls. append ( contentsOf: try renderSingleExportedClass ( klass: klass) )
9191 }
9292 }
93+
94+ try withSpan ( " Render Async Promise Helpers " ) { [ self ] in
95+ let asyncResolveTypes = skeleton. asyncPromiseResolveReturnTypes
96+ if !asyncResolveTypes. isEmpty {
97+ decls. append ( contentsOf: try renderPromiseRejectHelper ( ) )
98+ for type in asyncResolveTypes {
99+ decls. append ( contentsOf: try renderPromiseResolveHelper ( type) )
100+ }
101+ }
102+ }
93103 return withSpan ( " Format Export Glue " ) {
94104 return decls. map { $0. description } . joined ( separator: " \n \n " )
95105 }
96106 }
97107
108+ /// Generates the per-type `Promise_resolve_<mangled>` settlement helper.
109+ private func renderPromiseResolveHelper( _ type: BridgeType ) throws -> [ DeclSyntax ] {
110+ try renderPromiseSettleHelper (
111+ functionName: " Promise_resolve_ \( type. mangleTypeName) " ,
112+ externName: " promise_resolve_ \( moduleName) _ \( type. mangleTypeName) " ,
113+ valueType: type
114+ )
115+ }
116+
117+ /// Generates the shared `Promise_reject` settlement helper.
118+ private func renderPromiseRejectHelper( ) throws -> [ DeclSyntax ] {
119+ try renderPromiseSettleHelper (
120+ functionName: " Promise_reject " ,
121+ externName: " promise_reject_ \( moduleName) " ,
122+ valueType: . jsValue
123+ )
124+ }
125+
126+ /// Generates a `@JSFunction func <functionName>(_ promise: JSObject, _ value: T)` and its
127+ /// glue, lowering `value` through the standard imported-parameter ABI.
128+ private func renderPromiseSettleHelper(
129+ functionName: String ,
130+ externName: String ,
131+ valueType: BridgeType
132+ ) throws -> [ DeclSyntax ] {
133+ let effects = Effects ( isAsync: false , isThrows: true )
134+ let parameters = [
135+ Parameter ( label: nil , name: " promise " , type: . jsObject( nil ) ) ,
136+ Parameter ( label: nil , name: " value " , type: valueType) ,
137+ ]
138+ let builder = try ImportTS . CallJSEmission (
139+ moduleName: " bjs " ,
140+ abiName: externName,
141+ effects: effects,
142+ returnType: . void,
143+ context: . importTS
144+ )
145+ for parameter in parameters {
146+ try builder. lowerParameter ( param: parameter)
147+ }
148+ try builder. call ( )
149+ try builder. liftReturnValue ( )
150+
151+ let macroDecl : DeclSyntax =
152+ " @JSFunction func \( raw: functionName) (_ promise: JSObject, _ value: \( raw: valueType. swiftType) ) throws(JSException) "
153+ let glueDecl = builder. renderThunkDecl (
154+ name: " _$ \( functionName) " ,
155+ parameters: parameters,
156+ returnType: . void,
157+ effects: effects
158+ )
159+ return [ macroDecl, builder. renderImportDecl ( ) , glueDecl]
160+ }
161+
98162 class ExportedThunkBuilder {
99163 var body : [ CodeBlockItemSyntax ] = [ ]
100164 var liftedParameterExprs : [ ExprSyntax ] = [ ]
@@ -104,6 +168,12 @@ public class ExportSwift {
104168 var externDecls : [ DeclSyntax ] = [ ]
105169 let effects : Effects
106170
171+ /// When set, the async thunk settles via `_bjs_makePromise` rather than the `.jsValue` path.
172+ var asyncResolveReturnType : BridgeType ?
173+
174+ /// Stack-using parameter lifts hoisted ahead of the deferred async closure.
175+ var asyncHoistedBindings : [ CodeBlockItemSyntax ] = [ ]
176+
107177 init ( effects: Effects ) {
108178 self . effects = effects
109179 }
@@ -200,6 +270,10 @@ public class ExportSwift {
200270 }
201271
202272 if effects. isAsync, returnType != . void {
273+ if asyncResolveReturnType != nil {
274+ // `_bjs_makePromise` lowers the awaited value via its injected `resolve` closure.
275+ return CodeBlockItemSyntax ( item: . init( StmtSyntax ( " return \( raw: callExpr) " ) ) )
276+ }
203277 return CodeBlockItemSyntax ( item: . init( StmtSyntax ( " return \( raw: callExpr) .jsValue " ) ) )
204278 }
205279
@@ -244,6 +318,23 @@ public class ExportSwift {
244318 param. type. isStackUsingParameter ? index : nil
245319 }
246320
321+ if effects. isAsync {
322+ // The async body runs on a deferred `Task`, so drain stack parameters now and
323+ // capture them; the shared bridge stack would otherwise be corrupted. Reverse for LIFO.
324+ for index in stackParamIndices. reversed ( ) {
325+ let param = parameters [ index]
326+ let expr = liftedParameterExprs [ index]
327+ let varName = " _tmp_ \( param. name) "
328+ var binding : CodeBlockItemSyntax = " let \( raw: varName) = \( expr) "
329+ if !asyncHoistedBindings. isEmpty {
330+ binding = binding. with ( \. leadingTrivia, . newline)
331+ }
332+ asyncHoistedBindings. append ( binding)
333+ liftedParameterExprs [ index] = ExprSyntax ( DeclReferenceExprSyntax ( baseName: . identifier( varName) ) )
334+ }
335+ return
336+ }
337+
247338 guard stackParamIndices. count > 1 else { return }
248339
249340 for index in stackParamIndices. reversed ( ) {
@@ -328,21 +419,32 @@ public class ExportSwift {
328419 }
329420 }
330421
422+ /// A throwing async body needs an explicit closure type, otherwise Swift infers
423+ /// `throws(any Error)` instead of `throws(JSException)`.
424+ /// See: https://github.com/swiftlang/swift/issues/76165
425+ private func asyncThrowsClosureHead( returnSpelling: String ? ) -> String {
426+ guard effects. isThrows else { return " " }
427+ let returns = returnSpelling. map { " -> \( $0) " } ?? " "
428+ return " () async throws(JSException) \( returns) in "
429+ }
430+
331431 func render( abiName: String ) -> DeclSyntax {
332432 let body : CodeBlockItemListSyntax
333- if effects. isAsync {
334- // Explicit closure type annotation needed when throws is present
335- // so Swift infers throws(JSException) instead of throws(any Error)
336- // See: https://github.com/swiftlang/swift/issues/76165
337- let closureHead : String
338- if effects. isThrows {
339- let hasReturn = self . body. contains { $0. description. contains ( " return " ) }
340- let ret = hasReturn ? " -> JSValue " : " "
341- closureHead = " () async throws(JSException) \( ret) in "
342- } else {
343- closureHead = " "
344- }
433+ if effects. isAsync, let resolveType = asyncResolveReturnType {
434+ // Not `ConvertibleToJSValue`: settle the promise through `_bjs_makePromise`.
435+ let resolveName = " Promise_resolve_ \( resolveType. mangleTypeName) "
436+ let closureHead = asyncThrowsClosureHead ( returnSpelling: resolveType. swiftType)
437+ body = """
438+ \( CodeBlockItemListSyntax ( asyncHoistedBindings) )
439+ return _bjs_makePromise(resolve: \( raw: resolveName) , reject: Promise_reject) { \( raw: closureHead)
440+ \( CodeBlockItemListSyntax ( self . body) )
441+ }
442+ """
443+ } else if effects. isAsync {
444+ let hasReturn = self . body. contains { $0. description. contains ( " return " ) }
445+ let closureHead = asyncThrowsClosureHead ( returnSpelling: hasReturn ? " JSValue " : nil )
345446 body = """
447+ \( CodeBlockItemListSyntax ( asyncHoistedBindings) )
346448 let ret = JSPromise.async { \( raw: closureHead)
347449 \( CodeBlockItemListSyntax ( self . body) )
348450 }.jsObject
@@ -506,8 +608,47 @@ public class ExportSwift {
506608 return decls
507609 }
508610
611+ private func configureAsyncResolve(
612+ _ builder: ExportedThunkBuilder ,
613+ returnType: BridgeType ,
614+ effects: Effects
615+ ) throws {
616+ guard effects. isAsync else { return }
617+ if returnType. usesAsyncPromiseResolve {
618+ builder. asyncResolveReturnType = returnType
619+ return
620+ }
621+ // Otherwise the `.jsValue` path is used, which only compiles for `ConvertibleToJSValue`
622+ // types. Diagnose the types that have neither a `.jsValue` nor a `_bjs_makePromise`
623+ // representation rather than emitting uncompilable code.
624+ if Self . asyncReturnLacksBridging ( returnType) {
625+ throw BridgeJSCoreError (
626+ " Returning ' \( returnType. swiftType) ' from an async exported function is not yet supported "
627+ )
628+ }
629+ }
630+
631+ /// Whether an `async` return type can be neither lowered through `_bjs_makePromise`
632+ /// (`usesAsyncPromiseResolve`) nor through the `.jsValue` path. Recurses through
633+ /// `Optional`/`Array`/`Dictionary` to catch unsupported element/value types.
634+ private static func asyncReturnLacksBridging( _ type: BridgeType ) -> Bool {
635+ switch type {
636+ case . associatedValueEnum, . swiftProtocol, . namespaceEnum:
637+ return true
638+ case . nullable( let wrapped, _) :
639+ return asyncReturnLacksBridging ( wrapped)
640+ case . array( let element) :
641+ return asyncReturnLacksBridging ( element)
642+ case . dictionary( let value) :
643+ return asyncReturnLacksBridging ( value)
644+ default :
645+ return false
646+ }
647+ }
648+
509649 func renderSingleExportedFunction( function: ExportedFunction ) throws -> DeclSyntax {
510650 let builder = ExportedThunkBuilder ( effects: function. effects)
651+ try configureAsyncResolve ( builder, returnType: function. returnType, effects: function. effects)
511652 for param in function. parameters {
512653 try builder. liftParameter ( param: param)
513654 }
@@ -551,6 +692,7 @@ public class ExportSwift {
551692 instanceSelfType: BridgeType
552693 ) throws -> DeclSyntax {
553694 let builder = ExportedThunkBuilder ( effects: method. effects)
695+ try configureAsyncResolve ( builder, returnType: method. returnType, effects: method. effects)
554696 if !method. effects. isStatic {
555697 try builder. liftParameter ( param: Parameter ( label: nil , name: " _self " , type: instanceSelfType) )
556698 }
0 commit comments