Skip to content

Commit 1e50386

Browse files
committed
Tests: Add reproducer for Swift 6.3 typed-throws async closure miscompile
1 parent 982e9fb commit 1e50386

1 file changed

Lines changed: 231 additions & 0 deletions

File tree

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import XCTest
2+
import JavaScriptKit
3+
import JavaScriptEventLoop
4+
5+
/// Reproducer for swiftlang/swift#89320 (fix in flight: swiftlang/swift#89715):
6+
/// on wasm32, a typed error thrown from a CAPTURELESS async closure value or
7+
/// function reference (lowered via `thin_to_thick_function`) is lost across the
8+
/// async unwind when the error type is too large for the direct error convention.
9+
/// Capturing closures, direct async function calls, and small error payloads are
10+
/// unaffected. The bug cases below assert the current (incorrect) behavior so the
11+
/// suite stays green; flip them once swiftlang/swift#89715 lands.
12+
13+
// MARK: - Payload types
14+
15+
struct PlainValuePayloadError: Error, Equatable {
16+
let marker: String
17+
}
18+
19+
/// Intentionally limited to trivially-copyable fields: the corrupted error produced by
20+
/// swiftlang/swift#89320 is unsafe to read through String or class-reference payloads.
21+
struct LargePayloadError: Error, Equatable {
22+
let first: UInt64
23+
let second: UInt64
24+
let third: UInt64
25+
let marker: UInt64
26+
}
27+
28+
final class MarkerBox: @unchecked Sendable {
29+
let marker: String
30+
init(_ marker: String) { self.marker = marker }
31+
}
32+
struct ReferencePayloadError: Error {
33+
let box: MarkerBox
34+
}
35+
36+
// MARK: - Control "async function" forms (payload must survive)
37+
38+
private func asyncFunctionThrowsValue() async throws(PlainValuePayloadError) {
39+
throw PlainValuePayloadError(marker: "ALIVE")
40+
}
41+
42+
private func asyncFunctionThrowsReference() async throws(ReferencePayloadError) {
43+
throw ReferencePayloadError(box: MarkerBox("ALIVE"))
44+
}
45+
46+
private func asyncFunctionThrowsJSException() async throws(JSException) {
47+
throw JSException(JSError(message: "ALIVE").jsValue)
48+
}
49+
50+
@inline(never)
51+
private func largePayloadAfterThrow(
52+
_ closure: () async throws(LargePayloadError) -> Void
53+
) async -> LargePayloadError? {
54+
do {
55+
try await closure()
56+
return nil
57+
} catch {
58+
return error
59+
}
60+
}
61+
62+
final class TypedThrowsAsyncClosureBugTests: XCTestCase {
63+
64+
func testControl_valuePayload_asyncFunction() async throws {
65+
do {
66+
try await asyncFunctionThrowsValue()
67+
XCTFail("expected throw")
68+
} catch let error as PlainValuePayloadError {
69+
print("[REPRO] value / async function payload: \(error.marker)")
70+
XCTAssertEqual(error.marker, "ALIVE")
71+
}
72+
}
73+
74+
func testControl_valuePayload_typedThrowsAsyncClosure() async throws {
75+
let closure: () async throws(PlainValuePayloadError) -> Void = {
76+
() async throws(PlainValuePayloadError) -> Void in
77+
throw PlainValuePayloadError(marker: "ALIVE")
78+
}
79+
do {
80+
try await closure()
81+
XCTFail("expected throw")
82+
} catch let error as PlainValuePayloadError {
83+
print("[REPRO] value / typed-throws async closure payload: \(error.marker)")
84+
XCTAssertEqual(error.marker, "ALIVE")
85+
}
86+
}
87+
88+
func testControl_valuePayload_untypedAsyncClosure() async throws {
89+
let closure: () async throws -> Void = {
90+
throw PlainValuePayloadError(marker: "ALIVE")
91+
}
92+
do {
93+
try await closure()
94+
XCTFail("expected throw")
95+
} catch let error as PlainValuePayloadError {
96+
print("[REPRO] value / untyped async closure payload: \(error.marker)")
97+
XCTAssertEqual(error.marker, "ALIVE")
98+
}
99+
}
100+
101+
func testControl_referencePayload_asyncFunction() async throws {
102+
do {
103+
try await asyncFunctionThrowsReference()
104+
XCTFail("expected throw")
105+
} catch let error as ReferencePayloadError {
106+
print("[REPRO] ref / async function payload: \(error.box.marker)")
107+
XCTAssertEqual(error.box.marker, "ALIVE")
108+
}
109+
}
110+
111+
func testControl_referencePayload_typedThrowsAsyncClosure() async throws {
112+
let closure: () async throws(ReferencePayloadError) -> Void = {
113+
() async throws(ReferencePayloadError) -> Void in
114+
throw ReferencePayloadError(box: MarkerBox("ALIVE"))
115+
}
116+
do {
117+
try await closure()
118+
XCTFail("expected throw")
119+
} catch let error as ReferencePayloadError {
120+
print("[REPRO] ref / typed-throws async closure payload: \(error.box.marker)")
121+
XCTAssertEqual(error.box.marker, "ALIVE")
122+
}
123+
}
124+
125+
func testControl_jsException_asyncFunction() async throws {
126+
do {
127+
try await asyncFunctionThrowsJSException()
128+
XCTFail("expected throw")
129+
} catch let error as JSException {
130+
let alive = error.thrownValue.object != nil
131+
print("[REPRO] jsexc / async function object!=nil: \(alive)")
132+
XCTAssertTrue(alive, "async function should preserve the thrown JS object")
133+
XCTAssertEqual(error.thrownValue.object?.message.string, "ALIVE")
134+
}
135+
}
136+
137+
func testControl_jsException_untypedAsyncClosure() async throws {
138+
let closure: () async throws -> Void = {
139+
throw JSException(JSError(message: "ALIVE").jsValue)
140+
}
141+
do {
142+
try await closure()
143+
XCTFail("expected throw")
144+
} catch let error as JSException {
145+
let alive = error.thrownValue.object != nil
146+
print("[REPRO] jsexc / untyped async closure object!=nil: \(alive)")
147+
XCTAssertTrue(alive, "untyped async closure should preserve the thrown JS object")
148+
XCTAssertEqual(error.thrownValue.object?.message.string, "ALIVE")
149+
} catch {
150+
XCTFail("unexpected error type: \(error)")
151+
}
152+
}
153+
154+
func testControl_jsException_capturingTypedThrowsAsyncClosure() async throws {
155+
let capturedMarker = "ALIVE"
156+
let closure: () async throws(JSException) -> Void = { () async throws(JSException) -> Void in
157+
throw JSException(JSError(message: capturedMarker).jsValue)
158+
}
159+
do {
160+
try await closure()
161+
XCTFail("expected throw")
162+
} catch let error as JSException {
163+
let alive = error.thrownValue.object != nil
164+
print("[REPRO] jsexc / capturing typed-throws closure object!=nil: \(alive)")
165+
XCTAssertTrue(alive, "capturing typed-throws async closure should preserve the thrown JS object")
166+
XCTAssertEqual(error.thrownValue.object?.message.string, "ALIVE")
167+
}
168+
}
169+
170+
/// Asserts the current (buggy) behavior of swiftlang/swift#89320: the JSException payload is lost.
171+
/// When the compiler is fixed this test will fail; flip to XCTAssertTrue(alive).
172+
func testBug_jsException_typedThrowsAsyncClosure() async throws {
173+
let closure: () async throws(JSException) -> Void = { () async throws(JSException) -> Void in
174+
throw JSException(JSError(message: "ALIVE").jsValue)
175+
}
176+
do {
177+
try await closure()
178+
XCTFail("expected throw")
179+
} catch let error as JSException {
180+
let alive = error.thrownValue.object != nil
181+
print("[REPRO] jsexc / typed-throws async closure object!=nil: \(alive) <-- BUG: payload lost")
182+
183+
XCTAssertFalse(
184+
alive,
185+
"""
186+
swiftlang/swift#89320 is expected to LOSE the JSException payload \
187+
thrown from a captureless typed-throws async closure. If this \
188+
assertion now fails, the compiler bug appears fixed -- flip this to \
189+
XCTAssertTrue(alive) and re-enable the message round-trip check.
190+
"""
191+
)
192+
}
193+
}
194+
195+
/// Asserts the current (buggy) behavior of swiftlang/swift#89320 with a plain Swift
196+
/// error larger than the direct error convention: no JavaScriptKit types involved.
197+
func testBug_largePlainPayload_typedThrowsAsyncClosure() async throws {
198+
let expected = LargePayloadError(
199+
first: 0x1111_1111_1111_1111,
200+
second: 0x2222_2222_2222_2222,
201+
third: 0x3333_3333_3333_3333,
202+
marker: 0xA11FE
203+
)
204+
let closure: () async throws(LargePayloadError) -> Void = { () async throws(LargePayloadError) -> Void in
205+
throw LargePayloadError(
206+
first: 0x1111_1111_1111_1111,
207+
second: 0x2222_2222_2222_2222,
208+
third: 0x3333_3333_3333_3333,
209+
marker: 0xA11FE
210+
)
211+
}
212+
let observed = await largePayloadAfterThrow(closure)
213+
XCTAssertNotNil(observed, "expected throw")
214+
let observedDescription = observed.map {
215+
"first: \(String($0.first, radix: 16)) second: \(String($0.second, radix: 16)) "
216+
+ "third: \(String($0.third, radix: 16)) marker: \(String($0.marker, radix: 16))"
217+
}
218+
print("[REPRO] large / typed-throws async closure \(observedDescription ?? "nil") <-- BUG: payload lost")
219+
220+
XCTAssertNotEqual(
221+
observed,
222+
expected,
223+
"""
224+
swiftlang/swift#89320 is expected to LOSE the large plain-Swift error \
225+
payload thrown from a captureless typed-throws async closure. If this \
226+
assertion now fails, the compiler bug appears fixed -- flip this to \
227+
assert the payload is preserved.
228+
"""
229+
)
230+
}
231+
}

0 commit comments

Comments
 (0)