Skip to content

BridgeJS: Support optional @JS struct in imported function signatures#755

Merged
krodak merged 1 commit into
swiftwasm:mainfrom
PassiveLogic:kr/nullable-struct-import
Jun 8, 2026
Merged

BridgeJS: Support optional @JS struct in imported function signatures#755
krodak merged 1 commit into
swiftwasm:mainfrom
PassiveLogic:kr/nullable-struct-import

Conversation

@krodak
Copy link
Copy Markdown
Member

@krodak krodak commented Jun 8, 2026

Overview

Optional @JS structs could not be used as parameters or return values of imported (@JSFunction) signatures. While an optional primitive works, an optional @JS struct did not:

@JS struct Point { var x: Int; var y: Int }

@JSFunction func roundTrip(_ point: Point?) throws(JSException) -> Point? // did not compile

The code generator lowered Optional<Point> using the non-optional object-id ABI ([isSome, objectId] for parameters, a single Int32 word for returns), for which no Optional lowering exists in the runtime, so the generated thunk failed to compile.

This change bridges optional @JS structs through the stack ABI instead, an isSome discriminator plus the struct fields, exactly like optional arrays and dictionaries. @JS structs already conform to the stack-based bridging protocols, so the runtime's _BridgedAsOptional/stack extensions and the JS link's stack handling already support this shape. Only the import-side lowering and lifting in the code generator needed to change:

1. Parameter lowering. .nullable(.swiftStruct) in the import context now lowers to a single isSome flag, with the struct transferred on the stack, rather than appending the non-optional objectId word.

2. Return lifting. The optional struct return is now lifted from the stack rather than from a single object-id word.

Generated thunk, before:

let (pointIsSome, pointObjectId) = point.bridgeJSLowerParameter() // no such overload
let ret = bjs_roundTrip(pointIsSome, pointObjectId)
return Optional<Point>.bridgeJSLiftReturn(ret)

after:

let pointIsSome = point.bridgeJSLowerParameter()
bjs_roundTrip(pointIsSome)
return Optional<Point>.bridgeJSLiftReturn()

The export side already used the stack ABI for optional structs and is unchanged. The change is guarded to the import context, so non-optional struct imports keep their existing object-id ABI.

Adds an end-to-end runtime round-trip test (some and none) and a codegen snapshot covering the new import shape.

Optional @js structs could not be used as parameters or return values of
imported (@JSFunction) signatures: the generator lowered Optional<Struct>
using the non-optional object-id ABI ([isSome, objectId] / a single Int32
return), for which no Optional lowering exists, so the generated thunk did
not compile.

Bridge optional @js structs through the stack ABI instead - an isSome
discriminator plus the struct fields - exactly like optional arrays and
dictionaries. Structs already conform to the stack-based bridging protocols,
so the existing _BridgedAsOptional/stack runtime extensions and the JS link's
stack handling already support this; only the import-side lowering/lifting in
the code generator needed to change.

Adds a jsRoundTripOptionalPoint runtime round-trip (some + none) and a
SwiftStructImports codegen snapshot.
Copy link
Copy Markdown
Member

@MaxDesiatov MaxDesiatov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@krodak krodak self-assigned this Jun 8, 2026
@krodak krodak merged commit 2388473 into swiftwasm:main Jun 8, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants