Skip to content

BridgeJS: Support protocol refinement and constrained imports - #25

Closed
krodak wants to merge 1 commit into
mainfrom
kr/protocol-generic-constraints
Closed

krodak wants to merge 1 commit into
mainfrom
kr/protocol-generic-constraints

Conversation

@krodak

@krodak krodak commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Overview

Support same-module @JS protocol refinement and protocol constraints on generic JavaScript imports.

Generated implementations live in wrapper-only protocol extensions, so Swift resolves inheritance without copying members. TypeScript interfaces use extends. Protocol-qualified helper names also prevent collisions between same-named methods; Wasm import names are unchanged.

Example

import JavaScriptKit

@JS protocol Identified { var id: String { get } }
@JS protocol Named { var name: String { get } }
@JS protocol NamedEntity: Identified, Named {}

@JS func describe(_ value: NamedEntity) -> String {
    "\(value.id): \(value.name)"
}
const text = exports.describe({ id: "42", name: "Main building" });

This returns "42: Main building". Previously, the generated wrapper was missing the inherited requirements.

Generic Imports

@JS protocol Node: Identified, Named, BridgedSwiftGenericBridgeable {}

@JSFunction func roundTrip<T: Node>(_ value: T) throws(JSException) -> T

Generates roundTrip<T extends Node>(value: T): T. Qualified constraints such as GraphKit.Node are preserved. Concrete conformers must expose the required members to JavaScript.

Limitations

Dependency protocols can be used as generic constraints, but cannot be refined locally. Anonymous existential parameters such as any (Identified & Named) remain unsupported.

Test Plan

  • BridgeJS tests with SwiftSyntax 600-603 and npm run check:bridgejs-dts.
  • ./Utilities/bridge-js-generate.sh (no drift) and swift build --product BridgeJSTool.
  • make unittest SWIFT_SDK_ID=swift-6.3-RELEASE_wasm BUILD_SYSTEM=native, covering refinement, inherited accessors, and constrained imports.
  • ./Utilities/format.swift and git diff --check.

A @js protocol may now refine other @js protocols declared in the same
module, and an imported generic function may be constrained to a @js
protocol:

    @js protocol NamedEntity: Identified, Named {}
    @js func describe(_ value: NamedEntity) -> String { ... }

    @js protocol Node: Identified, Named, BridgedSwiftGenericBridgeable {}
    @JSFunction func roundTrip<T: Node>(_ value: T) throws(JSException) -> T

Refinement is resolved by Swift, not by the generator. The JS-backed
implementations are emitted on the protocol itself, constrained to
wrapper types, and the Any<P> wrapper becomes a trivial struct:

    extension Identified where Self: _BridgedSwiftProtocolWrapper { var id: String { ... } }
    struct AnyIdentified: Identified, _BridgedSwiftProtocolWrapper { let jsObject: JSObject; ... }

AnyNamedEntity then satisfies Identified's and Named's requirements
through their extensions. Every extern stays owned by the protocol that
declares the member and is registered exactly once on the JS side, a
member the refining protocol re-declares resolves to the more specific
extension, and user conformers never see these defaults because they
do not conform to _BridgedSwiftProtocolWrapper. The d.ts mirrors this
with 'interface NamedEntity extends Identified, Named'. The skeleton
records only each protocol's own members plus the refinement edge
(inheritedJSProtocols). Refining a protocol from another module is
diagnosed on the inheritance clause.

Protocol wrapper extern thunks are now named by ABI name rather than
'_extern_<method>', so two protocols declaring the same member no
longer emit duplicate fileprivate declarations.

Generic imports accept @js protocols in the constraint, either composed
with BridgedSwiftGenericBridgeable or inheriting it through the
protocol; qualified names such as GraphKit.Node are preserved in the
generated Swift. A protocol that inherits BridgedSwiftGenericBridgeable
gets a synthesized Any<P>: BridgedSwiftGenericBridgeable conformance
and a registered type handle, so Any<P> can be used as a generic
argument. The generated TypeScript signature carries 'T extends Node'.

Skeletons written by earlier plugins still decode: a bare "T" generic
parameter is read as an unconstrained GenericParameter.

Runtime coverage drives a two-parent refinement with a re-declared
member, an inherited settable property, and a member-less refiner from
a single JS object, alongside the constrained-import round trips.
@krodak
krodak force-pushed the kr/protocol-generic-constraints branch from 2eabb78 to 19e0b1b Compare September 22, 2026 12:48
@krodak krodak closed this Sep 22, 2026
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