Skip to content

Commit 3eeee89

Browse files
committed
BridgeJS: Module-qualify JS helper identifiers minted from @js type names
The link step minted the struct/enum helper factories, the `const`s holding them, and the keys of the shared `structHelpers` / `enumHelpers` tables from the type name alone, so two modules declaring a same-named `@JS` type emitted duplicate top-level `const`s in one glue scope (a load-time SyntaxError). Qualify all of them with the declaring module, reusing the `__bjs_codec_<Module>_<Type>` naming already used for the codec helpers: __bjs_createStructHelpers_<Module>_<AbiName> __bjs_createEnumHelpers_<Module>_<Name> __bjs_helpers_<Module>_<Name> structHelpers.<Module>_<AbiName> / enumHelpers.<Module>_<Name> Declaration sites take the module from the skeleton being emitted; reference sites, which only see a `BridgeType` (a bare type name), resolve it through the existing `typeOwnerModules` map. That map cannot disambiguate a name declared by two modules, and three things downstream stay minted from the bare name regardless: the top-level `const <Enum>Values` object, the `class <Name>` declaration, and the `bjs["swift_js_struct_*_<AbiName>"]` wasm import names, which the Swift side derives from the same name. Reject those inputs at link time with a message naming both modules and the type, instead of emitting glue that throws at load time or binds one module's values to the other module's helpers.
1 parent 71cc2f0 commit 3eeee89

26 files changed

Lines changed: 667 additions & 402 deletions

Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift

Lines changed: 112 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,9 @@ public struct BridgeJSLink {
521521

522522
private func generateAddImports(needsImportsObject: Bool) throws -> CodeFragmentPrinter {
523523
let printer = CodeFragmentPrinter()
524-
let allStructs = skeletons.compactMap { $0.exported?.structs }.flatMap { $0 }
524+
let allStructs = skeletons.flatMap { unified in
525+
(unified.exported?.structs ?? []).map { (moduleName: unified.moduleName, structDef: $0) }
526+
}
525527
printer.write("return {")
526528
try printer.indent {
527529
printer.write(lines: [
@@ -669,19 +671,24 @@ public struct BridgeJSLink {
669671
}
670672
printer.write("}")
671673
if !allStructs.isEmpty {
672-
for structDef in allStructs {
674+
for (moduleName, structDef) in allStructs {
675+
// The `bjs` import names are part of the wasm ABI and are
676+
// minted from the ABI name alone on both sides, so they
677+
// cannot be module-qualified here. `validateNoCrossModuleTypeNameCollisions`
678+
// rejects the inputs that would make them collide.
679+
let key = HelperNaming.qualified(base: structDef.abiName, module: moduleName)
673680
printer.write("bjs[\"swift_js_struct_lower_\(structDef.abiName)\"] = function(objectId) {")
674681
printer.indent {
675682
printer.write(
676-
"\(JSGlueVariableScope.reservedStructHelpers).\(structDef.abiName).lower(\(JSGlueVariableScope.reservedSwift).memory.getObject(objectId));"
683+
"\(JSGlueVariableScope.reservedStructHelpers).\(key).lower(\(JSGlueVariableScope.reservedSwift).memory.getObject(objectId));"
677684
)
678685
}
679686
printer.write("}")
680687

681688
printer.write("bjs[\"swift_js_struct_lift_\(structDef.abiName)\"] = function() {")
682689
printer.indent {
683690
printer.write(
684-
"const value = \(JSGlueVariableScope.reservedStructHelpers).\(structDef.abiName).lift();"
691+
"const value = \(JSGlueVariableScope.reservedStructHelpers).\(key).lift();"
685692
)
686693
printer.write("return \(JSGlueVariableScope.reservedSwift).memory.retain(value);")
687694
}
@@ -1255,12 +1262,18 @@ public struct BridgeJSLink {
12551262

12561263
let bodyPrinter = CodeFragmentPrinter()
12571264
let allStructs = exportedSkeletons.flatMap { $0.structs }
1258-
for structDef in allStructs {
1265+
for (moduleName, structDef) in skeletons.flatMap({ unified in
1266+
(unified.exported?.structs ?? []).map { (unified.moduleName, $0) }
1267+
}) {
12591268
let structPrinter = CodeFragmentPrinter()
12601269
let structScope = JSGlueVariableScope(intrinsicRegistry: intrinsicRegistry)
1261-
let fragment = IntrinsicJSFragment.structHelper(structDefinition: structDef, allStructs: allStructs)
1270+
let fragment = IntrinsicJSFragment.structHelper(
1271+
structDefinition: structDef,
1272+
allStructs: allStructs,
1273+
moduleName: moduleName
1274+
)
12621275
_ = try fragment.printCode(
1263-
[structDef.abiName],
1276+
[],
12641277
IntrinsicJSFragment.PrintCodeContext(
12651278
scope: structScope,
12661279
printer: structPrinter,
@@ -1271,13 +1284,16 @@ public struct BridgeJSLink {
12711284
bodyPrinter.write(lines: structPrinter.lines)
12721285
}
12731286

1274-
let allAssocEnums = exportedSkeletons.flatMap {
1275-
$0.enums.filter { $0.enumType == .associatedValue }
1276-
}
1277-
for enumDef in allAssocEnums {
1287+
for (moduleName, enumDef) in skeletons.flatMap({ unified in
1288+
(unified.exported?.enums ?? []).filter { $0.enumType == .associatedValue }
1289+
.map { (unified.moduleName, $0) }
1290+
}) {
12781291
let enumPrinter = CodeFragmentPrinter()
12791292
let enumScope = JSGlueVariableScope(intrinsicRegistry: intrinsicRegistry)
1280-
let fragment = IntrinsicJSFragment.associatedValueEnumHelperFactory(enumDefinition: enumDef)
1293+
let fragment = IntrinsicJSFragment.associatedValueEnumHelperFactory(
1294+
enumDefinition: enumDef,
1295+
moduleName: moduleName
1296+
)
12811297
_ = try fragment.printCode(
12821298
[enumDef.valuesName],
12831299
IntrinsicJSFragment.PrintCodeContext(
@@ -1402,6 +1418,7 @@ public struct BridgeJSLink {
14021418
}
14031419

14041420
public func link() throws -> (outputJs: String, outputDts: String) {
1421+
try validateNoCrossModuleTypeNameCollisions()
14051422
intrinsicRegistry.reset()
14061423
importedModuleRegistry.configure(skeletons: skeletons)
14071424
intrinsicRegistry.classNamespaces = skeletons.reduce(into: [:]) { result, unified in
@@ -1419,6 +1436,74 @@ public struct BridgeJSLink {
14191436
return (outputJs, outputDts)
14201437
}
14211438

1439+
/// Rejects skeletons in which two modules declare an exported type under the
1440+
/// same name.
1441+
///
1442+
/// Declaration-site identifiers are module-qualified, but three things
1443+
/// downstream of them are minted from the bare type name and cannot be:
1444+
///
1445+
/// - `BridgeType` carries only a type name, so a reference to `Point` from
1446+
/// module *B* cannot tell which module's `structHelpers` entry it means.
1447+
/// - The top-level `const <Enum>Values` object and the `class <Name>`
1448+
/// declaration are part of the public JS surface, so both modules would
1449+
/// emit the same `const` / `class` in one glue module.
1450+
/// - `bjs["swift_js_struct_lower_<AbiName>"]` is a wasm import name minted
1451+
/// from the ABI name on the Swift side too, so both modules import the
1452+
/// same one.
1453+
///
1454+
/// Emitting glue anyway would either throw at load time or silently bind one
1455+
/// module's values to the other module's helpers, so this fails the link.
1456+
func validateNoCrossModuleTypeNameCollisions() throws {
1457+
/// The identifier minted for a declaration, and where it comes from.
1458+
struct Declaration {
1459+
let moduleName: String
1460+
let kind: String
1461+
}
1462+
var declarations: [String: [Declaration]] = [:]
1463+
func record(_ mintedName: String, kind: String, module: String) {
1464+
// Two declarations from the *same* module colliding is a separate,
1465+
// pre-existing issue (`@JS` types of the same name in different
1466+
// namespaces); this check is only about cross-module collisions.
1467+
guard !(declarations[mintedName] ?? []).contains(where: { $0.moduleName == module }) else { return }
1468+
declarations[mintedName, default: []].append(Declaration(moduleName: module, kind: kind))
1469+
}
1470+
1471+
for unified in skeletons {
1472+
guard let skeleton = unified.exported else { continue }
1473+
let module = unified.moduleName
1474+
for structDef in skeleton.structs {
1475+
record(structDef.abiName, kind: "struct", module: module)
1476+
}
1477+
for klass in skeleton.classes {
1478+
record(klass.name, kind: "class", module: module)
1479+
}
1480+
// Namespace enums declare no runtime value of their own; they only
1481+
// contribute to the merged namespace objects, which already merge
1482+
// across modules.
1483+
for enumDef in skeleton.enums where enumDef.enumType != .namespace {
1484+
record(enumDef.name, kind: "enum", module: module)
1485+
}
1486+
}
1487+
1488+
for (mintedName, declarations) in declarations.sorted(by: { $0.key < $1.key }) where declarations.count > 1 {
1489+
let modules = declarations.map(\.moduleName).sorted()
1490+
let kinds = Set(declarations.map(\.kind)).sorted().joined(separator: "/")
1491+
throw BridgeJSLinkError(
1492+
message: """
1493+
Duplicate @JS \(kinds) '\(mintedName)' declared by modules \
1494+
\(modules.map { "'\($0)'" }.joined(separator: " and ")).
1495+
1496+
The generated JavaScript glue mints identifiers from the type name \
1497+
(the helper tables, the top-level declarations, and the wasm import names \
1498+
the Swift side derives from the same name), so the two declarations \
1499+
would collide in a single glue module.
1500+
1501+
Rename one of them so that each module declares a distinct name.
1502+
"""
1503+
)
1504+
}
1505+
}
1506+
14221507
/// Maps every type name a `BridgeType` can carry to the module that declares
14231508
/// it, so identifiers minted from type names can be module-qualified.
14241509
///
@@ -1463,12 +1548,13 @@ public struct BridgeJSLink {
14631548
private func enumHelperAssignments() -> CodeFragmentPrinter {
14641549
let printer = CodeFragmentPrinter()
14651550

1466-
for skeleton in skeletons.compactMap(\.exported) {
1551+
for unified in skeletons {
1552+
guard let skeleton = unified.exported else { continue }
14671553
for enumDef in skeleton.enums where enumDef.enumType == .associatedValue {
1468-
printer.write(
1469-
"const \(enumDef.name)Helpers = __bjs_create\(enumDef.valuesName)Helpers();"
1470-
)
1471-
printer.write("\(JSGlueVariableScope.reservedEnumHelpers).\(enumDef.name) = \(enumDef.name)Helpers;")
1554+
let key = HelperNaming.qualified(base: enumDef.name, module: unified.moduleName)
1555+
let local = HelperNaming.helperConstant(key)
1556+
printer.write("const \(local) = \(HelperNaming.enumHelperFactory(key))();")
1557+
printer.write("\(JSGlueVariableScope.reservedEnumHelpers).\(key) = \(local);")
14721558
printer.nextLine()
14731559
}
14741560
}
@@ -1479,14 +1565,13 @@ public struct BridgeJSLink {
14791565
private func structHelperAssignments() -> CodeFragmentPrinter {
14801566
let printer = CodeFragmentPrinter()
14811567

1482-
for skeleton in skeletons.compactMap(\.exported) {
1568+
for unified in skeletons {
1569+
guard let skeleton = unified.exported else { continue }
14831570
for structDef in skeleton.structs {
1484-
printer.write(
1485-
"const \(structDef.abiName)Helpers = __bjs_create\(structDef.abiName)Helpers();"
1486-
)
1487-
printer.write(
1488-
"\(JSGlueVariableScope.reservedStructHelpers).\(structDef.abiName) = \(structDef.abiName)Helpers;"
1489-
)
1571+
let key = HelperNaming.qualified(base: structDef.abiName, module: unified.moduleName)
1572+
let local = HelperNaming.helperConstant(key)
1573+
printer.write("const \(local) = \(HelperNaming.structHelperFactory(key))();")
1574+
printer.write("\(JSGlueVariableScope.reservedStructHelpers).\(key) = \(local);")
14901575
printer.nextLine()
14911576
}
14921577
}
@@ -4278,8 +4363,10 @@ private struct DocCComment {
42784363
}
42794364
}
42804365

4281-
struct BridgeJSLinkError: Error {
4366+
struct BridgeJSLinkError: Error, CustomStringConvertible {
42824367
let message: String
4368+
4369+
var description: String { message }
42834370
}
42844371

42854372
extension BridgeType {

0 commit comments

Comments
 (0)