Skip to content

Commit c995dfb

Browse files
committed
BridgeJS: Tests and fixtures for generic imports
1 parent 5f41e2a commit c995dfb

146 files changed

Lines changed: 12259 additions & 1796 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSCodegenTests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ import Testing
141141
swiftParts.append(s)
142142
}
143143
}
144+
if let typeRegistration = GenericTypeRegistrationCodegen().render(for: skeleton) {
145+
swiftParts.append(typeRegistration)
146+
}
144147
let combinedSwift =
145148
swiftParts
146149
.map { $0.trimmingCharacters(in: .newlines) }

Plugins/BridgeJS/Tests/BridgeJSToolTests/BridgeJSLinkTests.swift

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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")
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import Foundation
2+
import SwiftParser
3+
import SwiftSyntax
4+
import Testing
5+
6+
@testable import BridgeJSLink
7+
@testable import BridgeJSCore
8+
@testable import BridgeJSSkeleton
9+
10+
func makeSkeleton(
11+
_ source: String,
12+
moduleName: String = "TestModule",
13+
dependencies: [(moduleName: String, skeleton: BridgeJSSkeleton)] = []
14+
) throws -> BridgeJSSkeleton {
15+
let swiftAPI = SwiftToSkeleton(
16+
progress: .silent,
17+
moduleName: moduleName,
18+
exposeToGlobal: false,
19+
externalModuleIndex: ExternalModuleIndex(dependencies: dependencies)
20+
)
21+
swiftAPI.addSourceFile(Parser.parse(source: source), inputFilePath: "\(moduleName).swift")
22+
return try swiftAPI.finalize()
23+
}
24+
25+
func expectDiagnostic(
26+
source: String,
27+
moduleName: String = "App",
28+
contains message: String,
29+
sourceLocation: Testing.SourceLocation = #_sourceLocation
30+
) {
31+
do {
32+
_ = try makeSkeleton(source, moduleName: moduleName)
33+
Issue.record("Expected diagnostic but resolution succeeded", sourceLocation: sourceLocation)
34+
} catch let error as BridgeJSCoreDiagnosticError {
35+
let combined = error.diagnostics.map(\.diagnostic.message).joined(separator: "\n")
36+
#expect(combined.contains(message), sourceLocation: sourceLocation)
37+
} catch {
38+
Issue.record("Unexpected error: \(error)", sourceLocation: sourceLocation)
39+
}
40+
}
41+
42+
func linkSource(_ source: String, moduleName: String = "TestModule") throws -> (js: String, dts: String) {
43+
let skeleton = try makeSkeleton(source, moduleName: moduleName)
44+
var bridgeJSLink = BridgeJSLink(sharedMemory: false)
45+
let encoder = JSONEncoder()
46+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
47+
let unifiedData = try encoder.encode(skeleton)
48+
try bridgeJSLink.addSkeletonFile(data: unifiedData)
49+
let result = try bridgeJSLink.link()
50+
return (result.outputJs, result.outputDts)
51+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Testing
2+
3+
@testable import BridgeJSLink
4+
@testable import BridgeJSSkeleton
5+
6+
/// The codec a generic call site instantiates for each supported wrapped form
7+
/// of a generic parameter (`T`, `[T]`, `T?`, `[String: T]`).
8+
@Suite struct GenericCodecExpressionTests {
9+
@Test func bareGenericUsesTheElementCodecDirectly() {
10+
#expect(GenericJSCodegen.genericCodecExpression(type: .generic("T"), codec: "c") == "c")
11+
}
12+
13+
@Test func wrappedGenericsInstantiateTheSharedCombinators() {
14+
#expect(
15+
GenericJSCodegen.genericCodecExpression(type: .array(.generic("T")), codec: "c")
16+
== "__bjs_arrayCodec(c)"
17+
)
18+
#expect(
19+
GenericJSCodegen.genericCodecExpression(type: .dictionary(.generic("T")), codec: "c")
20+
== "__bjs_dictCodec(c)"
21+
)
22+
}
23+
24+
/// The optional combinator carries the null-vs-undefined flavour in its
25+
/// second argument, so the generic path must not drop `JSOptionalKind`.
26+
@Test func optionalGenericPreservesTheOptionalKind() {
27+
#expect(
28+
GenericJSCodegen.genericCodecExpression(type: .nullable(.generic("T"), .null), codec: "c")
29+
== "__bjs_optionalCodec(c)"
30+
)
31+
#expect(
32+
GenericJSCodegen.genericCodecExpression(type: .nullable(.generic("T"), .undefined), codec: "c")
33+
== "__bjs_optionalCodec(c, true)"
34+
)
35+
}
36+
37+
@Test func nonGenericTypesHaveNoGenericCodec() {
38+
#expect(GenericJSCodegen.genericCodecExpression(type: .string, codec: "c") == nil)
39+
#expect(GenericJSCodegen.genericCodecExpression(type: .array(.integer(.int)), codec: "c") == nil)
40+
}
41+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import Testing
2+
3+
@testable import BridgeJSCore
4+
5+
@Suite struct GenericExportDiagnosticsTests {
6+
7+
@Test
8+
func genericExportedFunctionRejected() {
9+
expectDiagnostic(
10+
source: """
11+
@JS public func identity<T: BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
12+
""",
13+
contains: "Generic parameters on exported @JS functions are not supported yet"
14+
)
15+
}
16+
17+
@Test
18+
func genericMethodOnExportedClassRejected() {
19+
expectDiagnostic(
20+
source: """
21+
@JS final class Box {
22+
@JS init() {}
23+
@JS func wrap<T: BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
24+
}
25+
""",
26+
contains: "Generic parameters on exported @JS functions are not supported yet"
27+
)
28+
}
29+
30+
@Test
31+
func genericMethodOnExportedStructRejected() {
32+
expectDiagnostic(
33+
source: """
34+
@JS struct Pair {
35+
@JS init() {}
36+
@JS func first<T: BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
37+
}
38+
""",
39+
contains: "Generic parameters on exported @JS functions are not supported yet"
40+
)
41+
}
42+
43+
@Test
44+
func genericStaticMethodOnExportedEnumRejected() {
45+
expectDiagnostic(
46+
source: """
47+
@JS enum Factory {
48+
case primary
49+
@JS static func one<T: BridgedSwiftGenericBridgeable>(_ value: T) -> T { value }
50+
}
51+
""",
52+
contains: "Generic parameters on exported @JS functions are not supported yet"
53+
)
54+
}
55+
56+
@Test
57+
func unconstrainedGenericExportedFunctionRejected() {
58+
expectDiagnostic(
59+
source: """
60+
@JS public func identity<T>(_ value: T) -> T { value }
61+
""",
62+
contains: "Generic parameters on exported @JS functions are not supported yet"
63+
)
64+
}
65+
}

0 commit comments

Comments
 (0)