BridgeJS: Support non-ConvertibleToJSValue async exported return types#758
Open
krodak wants to merge 1 commit into
Open
BridgeJS: Support non-ConvertibleToJSValue async exported return types#758krodak wants to merge 1 commit into
krodak wants to merge 1 commit into
Conversation
Async exported functions previously required a return type conforming to ConvertibleToJSValue, because the thunk wrapped the body in JSPromise.async and lowered the result via .jsValue. Types like @js structs and enums have no .jsValue, so async functions could not return them. Return such values through a new _bjs_makePromise intrinsic: the thunk creates a JS Promise synchronously, then settles it from a Task using generated Promise_resolve_<mangled> / Promise_reject thunks that lower the value through the regular imported-parameter ABI. ConvertibleToJSValue returns keep using the existing JSPromise.async path. Covers all bridged stack types and their compositions: @js struct, raw-value and case enums, their Optionals, Arrays, and Dictionaries. Stack-using parameters are hoisted and lifted in the thunk before the deferred Task runs, so the shared bridge stack is drained synchronously. Async returns of types that have neither a .jsValue nor a stack representation (associated-value enums, protocols, namespace enums) are diagnosed rather than miscompiled.
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
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 adds a settlement mechanism that returns such values through the regular bridged stack ABI, covering every non-
ConvertibleToJSValuestack type and their nested compositions. It implements the@JS structasync return requested in #753, generalized to all bridged stack types.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: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. Keep
ConvertibleToJSValuereturns on the existing path.Convertible returns (
Int,String,Bool,Int?,[String], ...) still useJSPromise.async+.jsValue. That path already works and is simpler, so only non-convertible returns are routed through_bjs_makePromise. The split is decided by a single recursive predicate,BridgeType.usesAsyncPromiseResolve, which returns true for@JS struct, raw-value/case enums, andOptional/Array/Dictionarywhose element ultimately resolves to one of those.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)(swiftlang/swift#76165). Non-throwing bodies infer correctly and are left unannotated.6. Diagnose, rather than miscompile, the unsupported cases.
Types that have neither a
.jsValuenor a stack representation (associated-value enums, protocols, namespace enums, including those nested inOptional/Array/Dictionary) are rejected with a clear BridgeJS diagnostic instead of producing uncompilable Swift.Foundation
This builds on three small ABI fixes that landed first and are what make the full type matrix work end to end: optional
@JS structimports, case-enum imports, and the Wasmi64BigInt zero placeholder. With those in place, the resolve thunks can lower any supported value, including optional structs, optionalInt64-backed enums, and dictionaries of structs.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); 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.