Skip to content

BridgeJS: Support generic functions at the Swift and JavaScript boundary#14

Draft
krodak wants to merge 5 commits into
mainfrom
kr/stack-abi-generics-export
Draft

BridgeJS: Support generic functions at the Swift and JavaScript boundary#14
krodak wants to merge 5 commits into
mainfrom
kr/stack-abi-generics-export

Conversation

@krodak

@krodak krodak commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

BridgeJS: Support generic functions at the Swift and JavaScript boundary

Overview

Adds support for generic functions to BridgeJS, in both directions, constrained to a new bridgeable bound T: _BridgedSwiftGenericBridgeable. Values cross the boundary using each type's existing stack ABI; a runtime type-ID registry selects the correct per-type codec, so the per-function glue stays type-agnostic. The @JS / @JSFunction macros are unchanged.

// Import: call a generic JS function from Swift
@JSFunction func parse<T: _BridgedSwiftGenericBridgeable>(_ json: String) -> T
let user: User = try parse(jsonString)          // T monomorphized at the call site

// Export: call a generic Swift function from JavaScript
@JS public func identity<T: _BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
import { BridgeTypes } from "./bridge-js.js";
const n = exports.identity(42, BridgeTypes.Int);          // token recovers T (TS erases generics)
const p = exports.identity({ x: 1, y: 2 }, BridgeTypes.Point);

Supported T

  • Bool, Float, Double, String, and JSValue
  • every fixed-width integer: Int, UInt, Int8/Int16/Int32/Int64, UInt8/UInt16/UInt32/UInt64
  • any @JS struct (including nested/namespaced and dependency-module structs), final @JS class, and @JS enum (case, raw-value, or associated-value)

The generic parameter may be used bare (T) or wrapped as [T], T?, or [String: T]. JSObject cannot be used as T (it is a non-final class that cannot satisfy the protocol's StackLiftResult == Self requirement); use JSValue instead.

What's included

1. Import direction

  • Generic @JSFunction thunks are generic and monomorphized by the Swift compiler at the call site; one type-agnostic wasm import carries a trailing type-ID per generic parameter, and JS dispatches through a shared codec table.
  • Supports a generic used in one or many parameters, multiple distinct generic parameters, and return-only generics (make<T>() -> T, where JS produces the value).

2. Export direction

  • The wasm entry point is a concrete @_expose/@_cdecl thunk taking a trailing type-ID per generic parameter; the generic value crosses on the stack. The thunk looks the type-ID up in a codegen-emitted [Int32: any _BridgedSwiftGenericBridgeable.Type] registry and reifies T through an opened existential. Multiple distinct generic parameters use a nested opening chain.
  • Concrete parameters of any supported bridged type may be mixed with the generic value, with correct stack ordering.
  • JavaScript callers pass a generated BridgeType<T> token (exported as a BridgeTypes map) as the trailing argument, since TypeScript erases generics.
  • Cross-module: a @JS struct defined in a dependency module can be used as T (codegen registers it, synthesizing a conditional @retroactive conformance when the defining module has no generics of its own).
  • Embedded Swift: opened existentials are unavailable, so the exported generic thunk is emitted as a runtime fatalError stub under #if hasFeature(Embedded) (codegen cannot know the consumer's Embedded mode); the import side remains Embedded-compatible.

3. Wrapped generics ([T], T?, [String: T])

  • Resolved by the parser to .array/.nullable/.dictionary of .generic, then bridged element-by-element through dedicated per-element stack helpers (_bridgeJSStackPush/PopArray/Optional/DictGeneric) on the Swift side and codec-driven composition helpers on the JS side. This keeps the wrapper ABI uniform and independent of the element type.

4. Codec generation

  • Every generic codec ({ lower, lift }) is generated from the canonical per-type stack fragments (stackLowerFragment / stackLiftFragment) — the same code path the non-generic glue uses — so there is no hand-written/duplicated lowering to drift. While unifying this, fixed liftCoerce for 64-bit unsigned integers so UInt64 (and the non-generic [UInt64] / UInt64? paths) lift to an unsigned BigInt.

5. Diagnostics
Build-time, source-located diagnostics for unsupported forms: missing/incorrect constraint, where clauses, async generics, generics inside @JSClass/static members, nested or unsupported wrappings ([[T]], [T?], [Int: T]), and (export) a return type that is neither a declared generic (optionally wrapped) nor Void.

6. Documentation
DocC articles updated (exporting/importing functions, supported types, unsupported features, internals rationale) and the BridgeJS README bridged-type table.

Test plan

  • Host snapshot + diagnostics tests: swift test --package-path ./Plugins/BridgeJS --disable-experimental-prebuilts.
  • swift-syntax compatibility matrix: BRIDGEJS_OVERRIDE_SWIFT_SYNTAX_VERSION={601,602,603}.0.0 swift test --package-path ./Plugins/BridgeJS --disable-experimental-prebuilts.
  • AoT generated trees in sync: ./Utilities/bridge-js-generate.sh then git diff --exit-code.
  • WebAssembly runtime round-trips: make unittest SWIFT_SDK_ID=<wasm-sdk>ImportGenericAPITests and ExportGenericAPITests cover every supported T in both directions, multiple/distinct generic parameters, value-type concrete parameters mixed with the generic, return-only imports, and the [T] / T? / [String: T] wrappers (including empty collections and nil).
  • ./Utilities/format.swift clean.

Notes

  • Organized as five commits for review: (1) generic bridging infrastructure, (2) generic function import and export codegen, (3) tests and fixtures, (4) documentation, (5) review-driven fixes and simplifications.
  • BridgeJS remains experimental; no API stability is implied.

@krodak krodak force-pushed the kr/stack-abi-generics-export branch 2 times, most recently from 3b9bb41 to dbcd640 Compare June 16, 2026 15:08
@krodak krodak force-pushed the kr/stack-abi-generics-export branch 6 times, most recently from b7c7763 to c6c78a4 Compare June 17, 2026 13:36
@krodak krodak force-pushed the kr/stack-abi-generics-export branch from c6c78a4 to 4ad8122 Compare June 17, 2026 13:57
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.

1 participant