Tests: Reproducer for Swift 6.3 typed-throws async closure miscompile by krodak · Pull Request #12 · PassiveLogic/JavaScriptKit
Overview
A standalone, runnable reproducer for the Swift compiler bug that blocks the Swift-to-JS async throws reject path in the closures work (the "Known limitation" in the feature PR). This branch is stacked on top of that feature branch so reviewers see only the reproducer test in the diff.
Root cause identified: this is swiftlang/swift#89320 ("Crash with typed/untyped throwing async functions on Wasm"), with a fix in progress in swiftlang/swift#89715 (IRGen: fix async typed throws miscompiles on Wasm). On wasm32, IRGen places the indirect typed-error pointer after swiftself at thick call sites, but a captureless closure or function reference goes through thin_to_thick_function whose thin signature has no swiftself; LLVM's wasm swiftcc padding appends the missing parameters, producing equal-arity but permuted signatures. The callee writes the thrown error through the caller's null swiftself, and the caller reads a never-written slot.
The bug triggers only when both conditions hold:
- the typed error is too large for the direct error convention (roughly >16 bytes on wasm32;
JSExceptionis ~36 bytes), and - the closure value was produced by
thin_to_thick_function(a captureless closure literal or a function reference).
Tests/BridgeJSRuntimeTests/TypedThrowsAsyncClosureBugTests.swift demonstrates both conditions with labelled [REPRO] output. The suite stays green: the buggy cases are encoded as current-behaviour assertions with a comment to flip once swiftlang/swift#89715 lands (XCTExpectFailure is not available in the wasm XCTest runtime).
What it shows
[REPRO] jsexc / typed-throws async closure object!=nil: false <- payload lost
[REPRO] large / typed-throws async closure garbage values <- payload lost (no JavaScriptKit types involved)
[REPRO] jsexc / async function object!=nil: true
[REPRO] jsexc / capturing typed-throws closure object!=nil: true <- capturing closures are unaffected
[REPRO] jsexc / untyped async closure object!=nil: true
[REPRO] value / typed-throws async closure payload: ALIVE <- small errors ride the direct convention
[REPRO] ref / typed-throws async closure payload: ALIVE
| case | error size | callee shape | result |
|---|---|---|---|
JSException from captureless async closure |
~36 B | thin_to_thick | LOST |
| 32-byte plain-Swift error from captureless async closure | 32 B | thin_to_thick | LOST |
JSException from async function |
~36 B | direct | preserved |
JSException from capturing async closure |
~36 B | partial apply | preserved |
JSException from untyped async closure |
~36 B | untyped convention | preserved |
| small struct / class-reference errors | 4-12 B | thin_to_thick | preserved |
Two additional observations that raise severity beyond the trap documented in swiftlang/swift#89320:
- the corruption is silent: the caller observes a zeroed or stale-garbage error value instead of trapping;
- the garbage is dangerous to even read: a corrupted error carrying a
Stringfield trapped withUnsafeBufferPointer has a nil start and nonzero countduring interpolation, and releasing corrupted references crashes inswift_release.
Relationship to existing Swift issues
- swiftlang/swift#89320: same root cause; that issue documents the trap manifestation, this reproducer adds the silent-corruption manifestation.
- swiftlang/swift#89715: the in-flight IRGen fix (orders the indirect error before
swiftselfso thin and thick signatures agree). - swiftlang/swift#89155:
Result.init(catching:)async trap on wasm, same root cause, used as a regression guard by the fix. - swiftlang/swift#89416 / #89541: an earlier SIL-level band-aid and its revert.
- The known compile-time typed-throws inference issues (Compiler unable to infer complete typed-throws within async closure swiftlang/swift#77718, #76169, #78880, #83773, #87556, #76807) are a distinct class: those fail to compile, this compiles and miscompiles at runtime.
Purpose
Internal review artifact to confirm the characterization before coordinating with the maintainers on the preferred path (merge with the documented limitation, box JSException storage to fit the direct error convention, or wait for swiftlang/swift#89715). Run with make unittest.