Skip to content

Commit f483b91

Browse files
authored
[BridgeJS] Synthesize typed-closure init access from declaration surface (swiftwasm#709) (swiftwasm#727)
* [BridgeJS] Synthesize typed-closure init access from declaration surface Resolves swiftwasm#709: a public `@JSClass` exposing a `JSTypedClosure<...>` parameter could not be consumed from another target because the synthesized `extension JSTypedClosure { init(...) }` was always internal, leaving downstream callers no way to construct the closure value without hand-rolling a public wrapper. Imported skeleton entries now record the source access level (`public`/`package`/`internal`); the closure-signature collector takes the maximum across every surface that references a given signature, and `ClosureCodegen` prefixes the synthesized init with the resulting modifier (internal stays bare). This matches the pattern `JSClassMacro` already uses for `init(unsafelyWrapping:)`. * [BridgeJS] Address PR feedback and refresh generated artifacts - Make `accessLevel` decode-tolerant on imported skeleton structs (`ImportedFunctionSkeleton`, `ImportedConstructorSkeleton`, `ImportedGetterSkeleton`, `ImportedSetterSkeleton`, `ImportedTypeSkeleton`) by writing explicit `init(from:)` decoders that fall back to `.internal` when the key is missing. Without this, any pre-existing skeleton JSON without the new field fails decoding — the `build-examples` CI job hit `DecodingError.keyNotFound` for `accessLevel` against externally consumed skeletons. - Extract a private `recordSignature` helper so `visitClosure` and `recordInjectedSignature` share a single merge implementation. - Assert in `withAccessLevel(rawLevel:)` so unknown access strings ("open", "private", future schema additions) surface in debug builds instead of silently inheriting the outer level. - Document the `.internal` seeding assumption on `ClosureSignatureCollectorVisitor.init(moduleName:signatures:)`. - Regenerate the BridgeJS pre-generated artifacts under Benchmarks/, Examples/PlayBridgeJS/, Tests/BridgeJSIdentityTests/, and Tests/BridgeJSRuntimeTests/ via `./Utilities/bridge-js-generate.sh`, per CONTRIBUTING.md. The runtime-tests Swift output now emits `public init` on three `JSTypedClosure` extensions whose signatures surface through public exported types. * [BridgeJS] Refresh identity tests skeleton after merge with main swiftwasm#731 added the GC lifecycle test (with new imported function entries) to main while this branch was open. Re-running the BridgeJS regen against the merged tree fills in the `accessLevel` field on the new entries that were absent at merge time. * ci: retry flaky JSPromiseTests.testPromiseAndTimer
1 parent 09b3f4e commit f483b91

37 files changed

Lines changed: 1606 additions & 75 deletions

Benchmarks/Sources/Generated/JavaScript/BridgeJS.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3339,6 +3339,7 @@
33393339
{
33403340
"functions" : [
33413341
{
3342+
"accessLevel" : "internal",
33423343
"effects" : {
33433344
"isAsync" : false,
33443345
"isStatic" : false,
@@ -3355,6 +3356,7 @@
33553356
}
33563357
},
33573358
{
3359+
"accessLevel" : "internal",
33583360
"effects" : {
33593361
"isAsync" : false,
33603362
"isStatic" : false,
@@ -3378,6 +3380,7 @@
33783380
}
33793381
},
33803382
{
3383+
"accessLevel" : "internal",
33813384
"effects" : {
33823385
"isAsync" : false,
33833386
"isStatic" : false,

Examples/PlayBridgeJS/Sources/PlayBridgeJS/Generated/JavaScript/BridgeJS.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@
242242
{
243243
"functions" : [
244244
{
245+
"accessLevel" : "internal",
245246
"effects" : {
246247
"isAsync" : false,
247248
"isStatic" : false,
@@ -260,11 +261,13 @@
260261
],
261262
"types" : [
262263
{
264+
"accessLevel" : "internal",
263265
"getters" : [
264266

265267
],
266268
"methods" : [
267269
{
270+
"accessLevel" : "internal",
268271
"effects" : {
269272
"isAsync" : false,
270273
"isStatic" : false,

Plugins/BridgeJS/Sources/BridgeJSCore/ClosureCodegen.swift

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ public struct ClosureCodegen {
2121
return "(\(closureParams))\(swiftEffects) -> \(swiftReturnType)"
2222
}
2323

24-
func renderClosureHelpers(_ signature: ClosureSignature) throws -> [DeclSyntax] {
24+
func renderClosureHelpers(
25+
_ signature: ClosureSignature,
26+
accessLevel: BridgeJSAccessLevel = .internal
27+
) throws -> [DeclSyntax] {
2528
let mangledName = signature.mangleName
2629
let helperName = "_BJS_Closure_\(mangledName)"
2730
let swiftClosureType = swiftClosureType(for: signature)
@@ -99,9 +102,10 @@ public struct ClosureCodegen {
99102

100103
let helperEnumDecl: DeclSyntax = "\(raw: helperEnumDeclPrinter.lines.joined(separator: "\n"))"
101104

105+
let initAccessModifier = accessLevel.modifierKeyword.map { "\($0) " } ?? ""
102106
let typedClosureExtension: DeclSyntax = """
103107
extension JSTypedClosure where Signature == \(raw: swiftClosureType) {
104-
init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping \(raw: swiftClosureType)) {
108+
\(raw: initAccessModifier)init(fileID: StaticString = #fileID, line: UInt32 = #line, _ body: @escaping \(raw: swiftClosureType)) {
105109
self.init(
106110
makeClosure: \(raw: externABIName),
107111
body: body,
@@ -192,12 +196,13 @@ public struct ClosureCodegen {
192196
let collector = ClosureSignatureCollectorVisitor(moduleName: skeleton.moduleName)
193197
var walker = BridgeSkeletonWalker(visitor: collector)
194198
walker.walk(skeleton)
195-
let closureSignatures = walker.visitor.signatures
196-
guard !closureSignatures.isEmpty else { return nil }
199+
let signatureAccessLevels = walker.visitor.signatureAccessLevels
200+
guard !signatureAccessLevels.isEmpty else { return nil }
197201

198202
var decls: [DeclSyntax] = []
199-
for signature in closureSignatures.sorted(by: { $0.mangleName < $1.mangleName }) {
200-
decls.append(contentsOf: try renderClosureHelpers(signature))
203+
for signature in signatureAccessLevels.keys.sorted(by: { $0.mangleName < $1.mangleName }) {
204+
let accessLevel = signatureAccessLevels[signature] ?? .internal
205+
decls.append(contentsOf: try renderClosureHelpers(signature, accessLevel: accessLevel))
201206
decls.append(try renderClosureInvokeHandler(signature))
202207
}
203208

Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2122,6 +2122,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
21222122
let name: String
21232123
let jsName: String?
21242124
let from: JSImportFrom?
2125+
let accessLevel: BridgeJSAccessLevel
21252126
var constructor: ImportedConstructorSkeleton?
21262127
var methods: [ImportedFunctionSkeleton]
21272128
var staticMethods: [ImportedFunctionSkeleton]
@@ -2337,6 +2338,7 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
23372338
name: typeName,
23382339
jsName: nil,
23392340
from: nil,
2341+
accessLevel: .internal,
23402342
constructor: nil,
23412343
methods: [],
23422344
staticMethods: [],
@@ -2345,12 +2347,18 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
23452347
)
23462348
}
23472349

2348-
private func enterJSClass(_ typeName: String, jsName: String?, from: JSImportFrom?) {
2350+
private func enterJSClass(
2351+
_ typeName: String,
2352+
jsName: String?,
2353+
from: JSImportFrom?,
2354+
accessLevel: BridgeJSAccessLevel
2355+
) {
23492356
stateStack.append(.jsClassBody(name: typeName))
23502357
currentType = CurrentType(
23512358
name: typeName,
23522359
jsName: jsName,
23532360
from: from,
2361+
accessLevel: accessLevel,
23542362
constructor: nil,
23552363
methods: [],
23562364
staticMethods: [],
@@ -2371,7 +2379,8 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
23712379
staticMethods: type.staticMethods,
23722380
getters: type.getters,
23732381
setters: type.setters,
2374-
documentation: nil
2382+
documentation: nil,
2383+
accessLevel: type.accessLevel
23752384
)
23762385
)
23772386
currentType = nil
@@ -2384,7 +2393,8 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
23842393
let attribute = AttributeChecker.firstJSClassAttribute(node.attributes)
23852394
let jsName = attribute.flatMap(AttributeChecker.extractJSName)
23862395
let from = attribute.flatMap(AttributeChecker.extractJSImportFrom)
2387-
enterJSClass(node.name.text, jsName: jsName, from: from)
2396+
let accessLevel = Self.bridgeAccessLevel(from: node.modifiers)
2397+
enterJSClass(node.name.text, jsName: jsName, from: from, accessLevel: accessLevel)
23882398
}
23892399
return .visitChildren
23902400
}
@@ -2400,7 +2410,8 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
24002410
let attribute = AttributeChecker.firstJSClassAttribute(node.attributes)
24012411
let jsName = attribute.flatMap(AttributeChecker.extractJSName)
24022412
let from = attribute.flatMap(AttributeChecker.extractJSImportFrom)
2403-
enterJSClass(node.name.text, jsName: jsName, from: from)
2413+
let accessLevel = Self.bridgeAccessLevel(from: node.modifiers)
2414+
enterJSClass(node.name.text, jsName: jsName, from: from, accessLevel: accessLevel)
24042415
}
24052416
return .visitChildren
24062417
}
@@ -2565,8 +2576,14 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
25652576
else {
25662577
return nil
25672578
}
2579+
// Initializers without an explicit modifier inherit access from the
2580+
// enclosing `@JSClass` (the user's example pattern: `public init(...)`
2581+
// inside `public struct JSDocument`).
2582+
let parentLevel = currentType?.accessLevel ?? .internal
2583+
let accessLevel = Self.bridgeAccessLevel(from: initializer.modifiers, default: parentLevel)
25682584
return ImportedConstructorSkeleton(
2569-
parameters: parseParameters(from: initializer.signature.parameterClause)
2585+
parameters: parseParameters(from: initializer.signature.parameterClause),
2586+
accessLevel: accessLevel
25702587
)
25712588
}
25722589

@@ -2599,14 +2616,16 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
25992616
} else {
26002617
returnType = .void
26012618
}
2619+
let accessLevel = Self.bridgeAccessLevel(from: node.modifiers)
26022620
return ImportedFunctionSkeleton(
26032621
name: name,
26042622
jsName: jsName,
26052623
from: from,
26062624
parameters: parameters,
26072625
returnType: returnType,
26082626
effects: effects,
2609-
documentation: nil
2627+
documentation: nil,
2628+
accessLevel: accessLevel
26102629
)
26112630
}
26122631

@@ -2638,13 +2657,15 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
26382657
let propertyName = SwiftToSkeleton.normalizeIdentifier(identifier.identifier.text)
26392658
let jsName = AttributeChecker.extractJSName(from: jsGetter)
26402659
let from = AttributeChecker.extractJSImportFrom(from: jsGetter)
2660+
let accessLevel = Self.bridgeAccessLevel(from: node.modifiers)
26412661
return ImportedGetterSkeleton(
26422662
name: propertyName,
26432663
jsName: jsName,
26442664
from: from,
26452665
type: propertyType,
26462666
documentation: nil,
2647-
functionName: nil
2667+
functionName: nil,
2668+
accessLevel: accessLevel
26482669
)
26492670
}
26502671

@@ -2667,12 +2688,14 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
26672688
return nil
26682689
}
26692690

2691+
let accessLevel = Self.bridgeAccessLevel(from: node.modifiers)
26702692
return ImportedSetterSkeleton(
26712693
name: propertyName,
26722694
jsName: validation.jsName,
26732695
type: validation.valueType,
26742696
documentation: nil,
2675-
functionName: "\(functionBaseName)_set"
2697+
functionName: "\(functionBaseName)_set",
2698+
accessLevel: accessLevel
26762699
)
26772700
}
26782701

@@ -2718,6 +2741,31 @@ private final class ImportSwiftMacrosAPICollector: SyntaxAnyVisitor {
27182741
modifier.name.tokenKind == .keyword(.static) || modifier.name.tokenKind == .keyword(.class)
27192742
}
27202743
}
2744+
2745+
/// Maps Swift's declaration modifiers to a `BridgeJSAccessLevel` for
2746+
/// recording on imported skeleton entries. Falls back to `default` when no
2747+
/// access modifier is present (typically `.internal`, but the caller may
2748+
/// override — e.g. an `init` inheriting from its enclosing `@JSClass`).
2749+
/// `private`/`fileprivate` are mapped to the fallback because the macros
2750+
/// already reject those access levels for `@JS*` declarations.
2751+
fileprivate static func bridgeAccessLevel(
2752+
from modifiers: DeclModifierListSyntax,
2753+
default fallback: BridgeJSAccessLevel = .internal
2754+
) -> BridgeJSAccessLevel {
2755+
for modifier in modifiers {
2756+
switch modifier.name.tokenKind {
2757+
case .keyword(.public), .keyword(.open):
2758+
return .public
2759+
case .keyword(.package):
2760+
return .package
2761+
case .keyword(.internal):
2762+
return .internal
2763+
default:
2764+
continue
2765+
}
2766+
}
2767+
return fallback
2768+
}
27212769
}
27222770

27232771
extension GenericArgumentListSyntax {

0 commit comments

Comments
 (0)