Skip to content

Commit c98c67f

Browse files
committed
Tests: Add reproducer for Swift 6.3 typed-throws async closure miscompile
1 parent 746c781 commit c98c67f

1 file changed

Lines changed: 155 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import XCTest
2+
import JavaScriptKit
3+
import JavaScriptEventLoop
4+
5+
/// Reproducer for a Swift 6.3 wasm32 miscompile: a `JSException` thrown from a
6+
/// `throws(JSException)` async closure VALUE loses its payload across the async
7+
/// unwind, while the same throw from an async function or an untyped async
8+
/// closure preserves it. The buggy case below asserts the current (incorrect)
9+
/// behavior so the suite stays green; flip it once the compiler is fixed.
10+
11+
// MARK: - Payload types
12+
13+
struct PlainValuePayloadError: Error, Equatable {
14+
let marker: String
15+
}
16+
17+
final class MarkerBox: @unchecked Sendable {
18+
let marker: String
19+
init(_ marker: String) { self.marker = marker }
20+
}
21+
struct ReferencePayloadError: Error {
22+
let box: MarkerBox
23+
}
24+
25+
// MARK: - Control "async function" forms (payload must survive)
26+
27+
private func asyncFunctionThrowsValue() async throws(PlainValuePayloadError) {
28+
throw PlainValuePayloadError(marker: "ALIVE")
29+
}
30+
31+
private func asyncFunctionThrowsReference() async throws(ReferencePayloadError) {
32+
throw ReferencePayloadError(box: MarkerBox("ALIVE"))
33+
}
34+
35+
private func asyncFunctionThrowsJSException() async throws(JSException) {
36+
throw JSException(JSError(message: "ALIVE").jsValue)
37+
}
38+
39+
final class TypedThrowsAsyncClosureBugTests: XCTestCase {
40+
41+
func testControl_valuePayload_asyncFunction() async throws {
42+
do {
43+
try await asyncFunctionThrowsValue()
44+
XCTFail("expected throw")
45+
} catch let error as PlainValuePayloadError {
46+
print("[REPRO] value / async function payload: \(error.marker)")
47+
XCTAssertEqual(error.marker, "ALIVE")
48+
}
49+
}
50+
51+
func testControl_valuePayload_typedThrowsAsyncClosure() async throws {
52+
let closure: () async throws(PlainValuePayloadError) -> Void = {
53+
() async throws(PlainValuePayloadError) -> Void in
54+
throw PlainValuePayloadError(marker: "ALIVE")
55+
}
56+
do {
57+
try await closure()
58+
XCTFail("expected throw")
59+
} catch let error as PlainValuePayloadError {
60+
print("[REPRO] value / typed-throws async closure payload: \(error.marker)")
61+
XCTAssertEqual(error.marker, "ALIVE")
62+
}
63+
}
64+
65+
func testControl_valuePayload_untypedAsyncClosure() async throws {
66+
let closure: () async throws -> Void = {
67+
throw PlainValuePayloadError(marker: "ALIVE")
68+
}
69+
do {
70+
try await closure()
71+
XCTFail("expected throw")
72+
} catch let error as PlainValuePayloadError {
73+
print("[REPRO] value / untyped async closure payload: \(error.marker)")
74+
XCTAssertEqual(error.marker, "ALIVE")
75+
}
76+
}
77+
78+
func testControl_referencePayload_asyncFunction() async throws {
79+
do {
80+
try await asyncFunctionThrowsReference()
81+
XCTFail("expected throw")
82+
} catch let error as ReferencePayloadError {
83+
print("[REPRO] ref / async function payload: \(error.box.marker)")
84+
XCTAssertEqual(error.box.marker, "ALIVE")
85+
}
86+
}
87+
88+
func testControl_referencePayload_typedThrowsAsyncClosure() async throws {
89+
let closure: () async throws(ReferencePayloadError) -> Void = {
90+
() async throws(ReferencePayloadError) -> Void in
91+
throw ReferencePayloadError(box: MarkerBox("ALIVE"))
92+
}
93+
do {
94+
try await closure()
95+
XCTFail("expected throw")
96+
} catch let error as ReferencePayloadError {
97+
print("[REPRO] ref / typed-throws async closure payload: \(error.box.marker)")
98+
XCTAssertEqual(error.box.marker, "ALIVE")
99+
}
100+
}
101+
102+
func testControl_jsException_asyncFunction() async throws {
103+
do {
104+
try await asyncFunctionThrowsJSException()
105+
XCTFail("expected throw")
106+
} catch let error as JSException {
107+
let alive = error.thrownValue.object != nil
108+
print("[REPRO] jsexc / async function object!=nil: \(alive)")
109+
XCTAssertTrue(alive, "async function should preserve the thrown JS object")
110+
XCTAssertEqual(error.thrownValue.object?.message.string, "ALIVE")
111+
}
112+
}
113+
114+
func testControl_jsException_untypedAsyncClosure() async throws {
115+
let closure: () async throws -> Void = {
116+
throw JSException(JSError(message: "ALIVE").jsValue)
117+
}
118+
do {
119+
try await closure()
120+
XCTFail("expected throw")
121+
} catch let error as JSException {
122+
let alive = error.thrownValue.object != nil
123+
print("[REPRO] jsexc / untyped async closure object!=nil: \(alive)")
124+
XCTAssertTrue(alive, "untyped async closure should preserve the thrown JS object")
125+
XCTAssertEqual(error.thrownValue.object?.message.string, "ALIVE")
126+
} catch {
127+
XCTFail("unexpected error type: \(error)")
128+
}
129+
}
130+
131+
/// Asserts the current (buggy) behavior: the JSException payload is lost.
132+
/// When the compiler is fixed this test will fail; flip to XCTAssertTrue(alive).
133+
func testBug_jsException_typedThrowsAsyncClosure() async throws {
134+
let closure: () async throws(JSException) -> Void = { () async throws(JSException) -> Void in
135+
throw JSException(JSError(message: "ALIVE").jsValue)
136+
}
137+
do {
138+
try await closure()
139+
XCTFail("expected throw")
140+
} catch let error as JSException {
141+
let alive = error.thrownValue.object != nil
142+
print("[REPRO] jsexc / typed-throws async closure object!=nil: \(alive) <-- BUG: payload lost")
143+
144+
XCTAssertFalse(
145+
alive,
146+
"""
147+
Swift 6.3 wasm32 typed-throws async closure value is expected to LOSE \
148+
the JSException payload. If this assertion now fails, the compiler bug \
149+
appears fixed -- flip this to XCTAssertTrue(alive) and re-enable the \
150+
message round-trip check.
151+
"""
152+
)
153+
}
154+
}
155+
}

0 commit comments

Comments
 (0)