BridgeJS: Support non-ConvertibleToJSValue async exported return types#758
Merged
kateinoigakukun merged 1 commit intoJun 9, 2026
swiftwasm:mainswiftwasm/JavaScriptKit:mainfrom
PassiveLogic:kr/async-stack-typesPassiveLogic/JavaScriptKit:kr/async-stack-typesCopy head branch name to clipboard
Merged
BridgeJS: Support non-ConvertibleToJSValue async exported return types#758kateinoigakukun merged 1 commit intoswiftwasm:mainswiftwasm/JavaScriptKit:mainfrom PassiveLogic:kr/async-stack-typesPassiveLogic/JavaScriptKit:kr/async-stack-typesCopy head branch name to clipboard
kateinoigakukun merged 1 commit into
swiftwasm:mainswiftwasm/JavaScriptKit:mainfrom
PassiveLogic:kr/async-stack-typesPassiveLogic/JavaScriptKit:kr/async-stack-typesCopy head branch name to clipboard
Conversation
bb0ed80 to
5d17f95
Compare
Member
Author
|
@kateinoigakukun let me know if approach aligns with your intention - if not just let me know which parts and I'll update accordingly 🙏🏻 |
kateinoigakukun
left a comment
Member
There was a problem hiding this comment.
Overall, looks good to me! Thanks!
5d17f95 to
fef5580
Compare
fef5580 to
54469d7
Compare
54469d7 to
18eaf93
Compare
kateinoigakukun
approved these changes
Jun 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Addresses #753. Async exported functions previously required a return type conforming to
ConvertibleToJSValue. The generated thunk wrapped the body inJSPromise.async { ... }and lowered the result with.jsValue, so types that have no.jsValuerepresentation (@JS structs, raw-value and case enums, and theirOptional/Array/Dictionarycompositions) could not be returned from anasync @JS func:This PR settles every
asyncexported return through a single new mechanism that lowers the value via the regular imported-parameter ABI, so all bridged types and their nested compositions can be returned.Approach
1. Create the Promise eagerly, settle it from a
Task.An exported async function must hand a value back to JavaScript synchronously (the Wasm export returns an
i32object id), while the work itself is asynchronous. The thunk therefore creates a JSPromiseimmediately via a new_bjs_makePromiseintrinsic, returns its retained object id, and resolves or rejects it later from aTask:The
resolve/rejectsettlers are stashed on thePromiseunder aSymbol(rather than plain string fields) so they cannot clash with anything else on the object.2. Settle through generated
@JSFunctionthunks rather than ad-hoc lowering._bjs_makePromiselives in the JavaScriptKit library and cannot name the per-module, per-type lowering that the generator produces. So the generator emits aPromise_resolve_<mangled>thunk per distinct return type (deduplicated by mangled name) and a single sharedPromise_reject, and injects them into_bjs_makePromise. Each resolve thunk lowers its value through the standard imported-parameter ABI:The benefit of reusing the imported-parameter ABI is that every existing per-type lowering is inherited for free: struct fields pushed on the stack, enum tags, optional
isSomediscriminators, arrays, and dictionaries all round-trip with no new marshaling code.rejectuses the fullJSValueABI so any thrown error is forwarded faithfully.3. One path for all async returns.
Rather than branch between
JSPromise.async+.jsValueforConvertibleToJSValuereturns and_bjs_makePromisefor the rest, every async return now settles through_bjs_makePromise. This removes the dual codegen path and the type predicate that used to select between them.Voidis the one special case: aVoidvalue can't cross the bridge as a parameter, so itsresolvethunk takes only the promise (via a small_bjs_makePromiseoverload) and settles withundefined.4. Drain stack parameters synchronously before the deferred
Task.Complex parameters travel via a single shared, mutable bridge stack. Because the async body runs later on a
Task, any stack-using parameter must be lifted into a local in the thunk before theTaskis scheduled, otherwise an interleaved bridge call would corrupt the shared stack. The thunk hoists these lifts in reverse (LIFO) order to preserve the stack discipline:5. Annotate the throwing async closure explicitly.
A throwing async body needs an explicit
() async throws(JSException) -> T inclosure type; without it Swift infersthrows(any Error)instead ofthrows(JSException). Non-throwing bodies infer correctly and are left unannotated.6. Diagnose, rather than miscompile, the unsupported cases.
The few types that cannot be lowered through the imported-parameter ABI (associated-value enums, protocols, namespace enums, including those nested in
Optional/Array/Dictionary) are rejected with a clear BridgeJS diagnostic instead of producing uncompilable Swift.Tests
End-to-end runtime round-trips plus codegen snapshots cover the matrix:
@JS struct, case enum, and raw-value enum, each as a bare value,Optional,Array, andDictionary; anInt64-backed enum (base and optional, exercising thei64resolve-helper signature);VoidandConvertibleToJSValuereturns; the throwing variant, the reject path (asserted viaassert.rejects), multi-parameter stack hoisting, a rich nested struct, an async class method, and concurrent calls viaPromise.all(the regression guard for shared-stack corruption). Unsupported types are covered by the diagnostic path.