diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift index 440960237..90c572b9d 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift @@ -426,21 +426,45 @@ public class ExportSwift { /// A throwing async body needs an explicit closure type, otherwise Swift infers /// `throws(any Error)` instead of `throws(JSException)`. /// See: https://github.com/swiftlang/swift/issues/76165 - private func asyncThrowsClosureHead(returnSpelling: String?) -> String { + private func asyncThrowsClosureHead(returnSpelling: String?, forcesCapture: Bool) -> String { guard effects.isThrows else { return "" } let returns = returnSpelling.map { " -> \($0)" } ?? "" - return " () async throws(JSException)\(returns) in" + let capture = forcesCapture ? "[__bjs_capture] " : "" + return " \(capture)() async throws(JSException)\(returns) in" + } + + /// A captureless throwing async body closure lowers via `thin_to_thick_function`, + /// which miscompiles typed-error calls on wasm32. Forcing a capture that the body + /// reads turns the closure into a partial apply with a context, avoiding the + /// broken convention. An unread capture list entry is dropped by capture analysis, + /// so the body must also read the captured value. + /// See: https://github.com/swiftlang/swift/issues/89320 + private var asyncThrowsBodyForcesCapture: Bool { + effects.isThrows && abiParameterSignatures.isEmpty && asyncHoistedBindings.isEmpty } func render(abiName: String) -> DeclSyntax { let body: CodeBlockItemListSyntax if effects.isAsync, let resolveType = asyncResolveReturnType { let resolveName = "Promise_resolve_\(resolveType.mangleTypeName)" - let closureHead = asyncThrowsClosureHead(returnSpelling: resolveType.swiftType) + let forcesCapture = asyncThrowsBodyForcesCapture + let closureHead = asyncThrowsClosureHead( + returnSpelling: resolveType.swiftType, + forcesCapture: forcesCapture + ) + var hoistedBindings = asyncHoistedBindings + var bodyItems = self.body + if forcesCapture { + hoistedBindings.append("let __bjs_capture = 0") + if !bodyItems.isEmpty { + bodyItems[0] = bodyItems[0].with(\.leadingTrivia, .newline) + } + bodyItems.insert("_ = __bjs_capture", at: 0) + } body = """ - \(CodeBlockItemListSyntax(asyncHoistedBindings)) + \(CodeBlockItemListSyntax(hoistedBindings)) return _bjs_makePromise(resolve: \(raw: resolveName), reject: Promise_reject) {\(raw: closureHead) - \(CodeBlockItemListSyntax(self.body)) + \(CodeBlockItemListSyntax(bodyItems)) } """ } else if effects.isThrows { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Async.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Async.swift index e63bea4ca..742d96ed2 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Async.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/Async.swift @@ -31,6 +31,10 @@ return v } +@JS func asyncThrowsZeroArg() async throws(JSException) -> String { + return "ok" +} + @JS func asyncCombineStructs(_ a: AsyncPoint, _ b: AsyncPoint) async -> AsyncPoint { return AsyncPoint(x: a.x + b.x, y: a.y + b.y) } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.json index 3bd594419..8684291f0 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.json @@ -283,6 +283,23 @@ } } }, + { + "abiName" : "bjs_asyncThrowsZeroArg", + "effects" : { + "isAsync" : true, + "isStatic" : false, + "isThrows" : true + }, + "name" : "asyncThrowsZeroArg", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + }, { "abiName" : "bjs_asyncCombineStructs", "effects" : { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.swift index 28e6d8d8f..661fbd3a5 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/Async.swift @@ -194,6 +194,20 @@ public func _bjs_asyncRoundTripStructThrows() -> Int32 { #endif } +@_expose(wasm, "bjs_asyncThrowsZeroArg") +@_cdecl("bjs_asyncThrowsZeroArg") +public func _bjs_asyncThrowsZeroArg() -> Int32 { + #if arch(wasm32) + 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 asyncThrowsZeroArg() + } + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_asyncCombineStructs") @_cdecl("bjs_asyncCombineStructs") public func _bjs_asyncCombineStructs() -> Int32 { diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.d.ts index ddf722a3a..507a96d4a 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.d.ts @@ -34,6 +34,7 @@ export type Exports = { asyncRoundTripJSObject(v: any): Promise; asyncRoundTripStruct(v: AsyncPoint): Promise; asyncRoundTripStructThrows(v: AsyncPoint): Promise; + asyncThrowsZeroArg(): Promise; asyncCombineStructs(a: AsyncPoint, b: AsyncPoint): Promise; asyncRoundTripEnum(v: AsyncDirectionTag): Promise; asyncRoundTripRawEnum(v: AsyncThemeTag): Promise; diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.js index 887102a76..9319cdd7e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/Async.js @@ -585,6 +585,18 @@ export async function createInstantiator(options, swift) { } return ret1; }, + asyncThrowsZeroArg: function bjs_asyncThrowsZeroArg() { + const ret = instance.exports.bjs_asyncThrowsZeroArg(); + const ret1 = swift.memory.getObject(ret); + swift.memory.release(ret); + if (tmpRetException) { + const error = swift.memory.getObject(tmpRetException); + swift.memory.release(tmpRetException); + tmpRetException = undefined; + throw error; + } + return ret1; + }, asyncCombineStructs: function bjs_asyncCombineStructs(a, b) { structHelpers.AsyncPoint.lower(a); structHelpers.AsyncPoint.lower(b); diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index 79a931930..a0453b8f8 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -96,6 +96,10 @@ struct TestError: Error { @JS func throwsWithSwiftHeapObjectResult() throws(JSException) -> Greeter { return Greeter(name: "Test") } @JS func throwsWithJSObjectResult() throws(JSException) -> JSObject { return JSObject() } +@JS func zeroArgAsyncThrows() async throws(JSException) -> String { + throw JSException(JSError(message: "ZeroArgAsyncThrowsError").jsValue) +} + @JS func asyncRoundTripVoid() async -> Void { return } @JS func asyncRoundTripInt(v: Int) async -> Int { return v } @JS func asyncRoundTripFloat(v: Float) async -> Float { return v } diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index 497fa3355..78bac8952 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -7189,6 +7189,20 @@ public func _bjs_throwsWithJSObjectResult() -> Int32 { #endif } +@_expose(wasm, "bjs_zeroArgAsyncThrows") +@_cdecl("bjs_zeroArgAsyncThrows") +public func _bjs_zeroArgAsyncThrows() -> Int32 { + #if arch(wasm32) + 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 zeroArgAsyncThrows() + } + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_asyncRoundTripVoid") @_cdecl("bjs_asyncRoundTripVoid") public func _bjs_asyncRoundTripVoid() -> Int32 { @@ -11403,6 +11417,28 @@ func _$Promise_reject(_ promise: JSObject, _ value: JSValue) throws(JSException) if let error = _swift_js_take_exception() { throw error } } +@JSFunction func Promise_resolve_SS(_ promise: JSObject, _ value: String) throws(JSException) + +#if arch(wasm32) +@_extern(wasm, module: "bjs", name: "promise_resolve_BridgeJSRuntimeTests_SS") +fileprivate func promise_resolve_BridgeJSRuntimeTests_SS_extern(_ promise: Int32, _ valueBytes: Int32, _ valueLength: Int32) -> Void +#else +fileprivate func promise_resolve_BridgeJSRuntimeTests_SS_extern(_ promise: Int32, _ valueBytes: Int32, _ valueLength: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif +@inline(never) fileprivate func promise_resolve_BridgeJSRuntimeTests_SS(_ promise: Int32, _ valueBytes: Int32, _ valueLength: Int32) -> Void { + return promise_resolve_BridgeJSRuntimeTests_SS_extern(promise, valueBytes, valueLength) +} + +func _$Promise_resolve_SS(_ promise: JSObject, _ value: String) throws(JSException) -> Void { + let promiseValue = promise.bridgeJSLowerParameter() + value.bridgeJSWithLoweredParameter { (valueBytes, valueLength) in + promise_resolve_BridgeJSRuntimeTests_SS(promiseValue, valueBytes, valueLength) + } + if let error = _swift_js_take_exception() { throw error } +} + @JSFunction func Promise_resolve_y(_ promise: JSObject) throws(JSException) #if arch(wasm32) @@ -11507,28 +11543,6 @@ func _$Promise_resolve_Sb(_ promise: JSObject, _ value: Bool) throws(JSException if let error = _swift_js_take_exception() { throw error } } -@JSFunction func Promise_resolve_SS(_ promise: JSObject, _ value: String) throws(JSException) - -#if arch(wasm32) -@_extern(wasm, module: "bjs", name: "promise_resolve_BridgeJSRuntimeTests_SS") -fileprivate func promise_resolve_BridgeJSRuntimeTests_SS_extern(_ promise: Int32, _ valueBytes: Int32, _ valueLength: Int32) -> Void -#else -fileprivate func promise_resolve_BridgeJSRuntimeTests_SS_extern(_ promise: Int32, _ valueBytes: Int32, _ valueLength: Int32) -> Void { - fatalError("Only available on WebAssembly") -} -#endif -@inline(never) fileprivate func promise_resolve_BridgeJSRuntimeTests_SS(_ promise: Int32, _ valueBytes: Int32, _ valueLength: Int32) -> Void { - return promise_resolve_BridgeJSRuntimeTests_SS_extern(promise, valueBytes, valueLength) -} - -func _$Promise_resolve_SS(_ promise: JSObject, _ value: String) throws(JSException) -> Void { - let promiseValue = promise.bridgeJSLowerParameter() - value.bridgeJSWithLoweredParameter { (valueBytes, valueLength) in - promise_resolve_BridgeJSRuntimeTests_SS(promiseValue, valueBytes, valueLength) - } - if let error = _swift_js_take_exception() { throw error } -} - @JSFunction func Promise_resolve_7GreeterC(_ promise: JSObject, _ value: Greeter) throws(JSException) #if arch(wasm32) diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json index d77883980..6535e9fc1 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json @@ -12171,6 +12171,23 @@ } } }, + { + "abiName" : "bjs_zeroArgAsyncThrows", + "effects" : { + "isAsync" : true, + "isStatic" : false, + "isThrows" : true + }, + "name" : "zeroArgAsyncThrows", + "parameters" : [ + + ], + "returnType" : { + "string" : { + + } + } + }, { "abiName" : "bjs_asyncRoundTripVoid", "effects" : { diff --git a/Tests/BridgeJSRuntimeTests/JavaScript/AsyncImportTests.mjs b/Tests/BridgeJSRuntimeTests/JavaScript/AsyncImportTests.mjs index eca2b209c..1a767b184 100644 --- a/Tests/BridgeJSRuntimeTests/JavaScript/AsyncImportTests.mjs +++ b/Tests/BridgeJSRuntimeTests/JavaScript/AsyncImportTests.mjs @@ -64,6 +64,11 @@ export async function runAsyncWorksTests(exports) { (error) => error instanceof Error && error.message === "async struct failure" ); + await assert.rejects( + () => exports.zeroArgAsyncThrows(), + (error) => error instanceof Error && error.message === "ZeroArgAsyncThrowsError" + ); + const richContact = { name: "Alice", age: 30,