@@ -139,6 +139,126 @@ import Testing
139139 try snapshot ( bridgeJSLink: bridgeJSLink, name: " MixedModules " )
140140 }
141141
142+ private func linkedJS( forFixture input: String ) throws -> String {
143+ let url = Self . inputsDirectory. appendingPathComponent ( input)
144+ let name = url. deletingPathExtension ( ) . lastPathComponent
145+ let sourceFile = Parser . parse ( source: try String ( contentsOf: url, encoding: . utf8) )
146+ let importSwift = SwiftToSkeleton (
147+ progress: . silent,
148+ moduleName: " TestModule " ,
149+ exposeToGlobal: false ,
150+ externalModuleIndex: . empty
151+ )
152+ importSwift. addSourceFile ( sourceFile, inputFilePath: " \( name) .swift " )
153+ let importResult = try importSwift. finalize ( )
154+ var bridgeJSLink = BridgeJSLink ( sharedMemory: false )
155+ let encoder = JSONEncoder ( )
156+ encoder. outputFormatting = [ . prettyPrinted, . sortedKeys]
157+ let unifiedData = try encoder. encode ( importResult)
158+ try bridgeJSLink. addSkeletonFile ( data: unifiedData)
159+ return try bridgeJSLink. link ( ) . 0
160+ }
161+
162+ @Test
163+ func genericRuntimeIsGatedToGenericBuilds( ) throws {
164+ let genericJS = try linkedJS ( forFixture: " GenericImports.swift " )
165+ #expect( genericJS. contains ( " __bjs_codecByTypeId " ) )
166+ #expect( genericJS. contains ( " __bjs_primitiveCodecs " ) )
167+ #expect( genericJS. contains ( " bjs[ \" bjs_TestModule_register_type_handles \" ] = function(base, count) { " ) )
168+ #expect( genericJS. contains ( " instance.exports[ \" bjs_TestModule_register_type_handles \" ](); " ) )
169+ // Eager registration hook: called by the instantiate.js template right
170+ // after WASI initialization (the lazy guard remains as a fallback).
171+ #expect( genericJS. contains ( " afterInitialize: () => { " ) )
172+
173+ // Modules with @JS types but no generic declarations still emit a Swift
174+ // registration export (their types may be used by a dependent module's
175+ // generic function), so the link layer must install a no-op hook for the
176+ // wasm import — but the generic runtime itself must be omitted. The
177+ // shared codec runtime (combinators + primitive codecs) gates
178+ // independently: it is emitted because the fixture bridges an optional
179+ // struct through the container stack ABI.
180+ let nonGenericJS = try linkedJS ( forFixture: " SwiftStructImports.swift " )
181+ #expect( !nonGenericJS. contains ( " __bjs_codecByTypeId " ) )
182+ #expect( nonGenericJS. contains ( " __bjs_optionalCodec(structHelpers.Point) " ) )
183+ #expect( nonGenericJS. contains ( " __bjs_primitiveCodecs " ) )
184+ #expect( nonGenericJS. contains ( " bjs[ \" bjs_TestModule_register_type_handles \" ] = function() {}; " ) )
185+ #expect( !nonGenericJS. contains ( " instance.exports[ \" bjs_TestModule_register_type_handles \" ](); " ) )
186+ // Without the generic runtime the hook is not emitted at all; the
187+ // instantiate template calls it with optional chaining.
188+ #expect( !nonGenericJS. contains ( " afterInitialize: () => { " ) )
189+
190+ // Builds that bridge no containers at all pay nothing for the shared
191+ // codec runtime either.
192+ let containerFreeJS = try linkedJS ( forFixture: " PrimitiveParameters.swift " )
193+ #expect( !containerFreeJS. contains ( " __bjs_arrayCodec " ) )
194+ #expect( !containerFreeJS. contains ( " __bjs_optionalCodec " ) )
195+ #expect( !containerFreeJS. contains ( " __bjs_dictCodec " ) )
196+ #expect( !containerFreeJS. contains ( " __bjs_primitiveCodecs " ) )
197+ #expect( !containerFreeJS. contains ( " __bjs_stringCodec " ) )
198+ }
199+
200+ @Test
201+ func sameTypeNameAcrossModulesLinksWithHandleIdentity( ) throws {
202+ // Type identity is pointer-based (each type owns a BridgeJSTypeHandle),
203+ // so two modules defining a same-named @JS type must link fine: each
204+ // module registers its own handle IDs against its own codec array.
205+ let structSource = """
206+ @JS public struct Point {
207+ public var x: Int
208+ @JS public init(x: Int) { self.x = x }
209+ }
210+ """
211+ let first = try makeSkeleton (
212+ structSource + """
213+
214+ @JSFunction func identity<T: BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
215+ """ ,
216+ moduleName: " FirstModule "
217+ )
218+ let second = try makeSkeleton ( structSource, moduleName: " SecondModule " )
219+ let bridgeJSLink = BridgeJSLink ( skeletons: [ first, second] , sharedMemory: false )
220+ let js = try bridgeJSLink. link ( ) . outputJs
221+ #expect( js. contains ( " bjs[ \" bjs_FirstModule_register_type_handles \" ] = function(base, count) { " ) )
222+ #expect( js. contains ( " bjs[ \" bjs_SecondModule_register_type_handles \" ] = function(base, count) { " ) )
223+ }
224+
225+ @Test
226+ func moduleWithoutGenericsStillRegistersItsTypeCodecs( ) throws {
227+ // A module cannot know whether a dependent module will pass its types to
228+ // a generic function, so a module with @JS types but no generic
229+ // declaration of its own still registers a codec for each of them, and
230+ // the linked glue drives every module's registration export. This is
231+ // what lets a type defined in Core be the generic argument of a generic
232+ // import declared in App.
233+ let core = try makeSkeleton (
234+ """
235+ @JS public struct Vector3D {
236+ public var x: Int
237+ @JS public init(x: Int) { self.x = x }
238+ }
239+ """ ,
240+ moduleName: " Core "
241+ )
242+ let app = try makeSkeleton (
243+ """
244+ @JSFunction func identity<T: BridgedSwiftGenericBridgeable>(_ value: T) throws(JSException) -> T
245+ """ ,
246+ moduleName: " App "
247+ )
248+
249+ // Core declares nothing imported at all, yet still registers its types.
250+ #expect( core. imported == nil )
251+ let coreEntries = try #require( core. typeRegistrationEntries)
252+ #expect( coreEntries. contains { $0. swiftName == " Vector3D " } )
253+
254+ let js = try BridgeJSLink ( skeletons: [ core, app] , sharedMemory: false ) . link ( ) . outputJs
255+ #expect( js. contains ( " instance.exports[ \" bjs_Core_register_type_handles \" ](); " ) )
256+ #expect( js. contains ( " instance.exports[ \" bjs_App_register_type_handles \" ](); " ) )
257+ // `lower(v)` is unique to a codec literal in a registration array;
258+ // `structHelpers.Vector3D` on its own is emitted for every @JS struct.
259+ #expect( js. contains ( " structHelpers.Vector3D.lower(v); " ) )
260+ }
261+
142262 @Test
143263 func perClassIdentityModeFromAnnotation( ) throws {
144264 let url = Self . inputsDirectory. appendingPathComponent ( " IdentityModeClass.swift " )
0 commit comments