Skip to content

Commit 09a24f4

Browse files
Changes: added dictionary runtime coverage and fixed BridgeJS optional dictionary bridging. New runtime tests in Tests/BridgeJSRuntimeTests/DictionaryTests.swift and JS helpers in Tests/prelude.mjs verify plain, nested, optional, and undefined dictionaries. BridgeJS intrinsics now bridge dictionaries through the optional/JSUndefinedOr paths via _BridgedSwiftDictionaryStackType. JS glue generation now lowers optional dictionary returns correctly; regenerated BridgeJS outputs.
Tests: `make unittest SWIFT_SDK_ID=DEVELOPMENT-SNAPSHOT-2025-11-03-a-wasm32-unknown-wasip1`. If you want, I can draft a commit message.
1 parent ec819b2 commit 09a24f4

File tree

6 files changed

+383
-0
lines changed

6 files changed

+383
-0
lines changed

Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,52 @@ struct IntrinsicJSFragment: Sendable {
11181118
}
11191119
printer.write("}")
11201120
cleanupCode.write("if (\(cleanupVar)) { \(cleanupVar)(); }")
1121+
case .dictionary(let valueType):
1122+
printer.write("if (\(isSomeVar)) {")
1123+
printer.indent {
1124+
let cleanupArrayVar = scope.variable("arrayCleanups")
1125+
let entriesVar = scope.variable("entries")
1126+
let entryVar = scope.variable("entry")
1127+
printer.write("const \(cleanupArrayVar) = [];")
1128+
printer.write("const \(entriesVar) = Object.entries(\(value));")
1129+
printer.write("for (const \(entryVar) of \(entriesVar)) {")
1130+
printer.indent {
1131+
let keyVar = scope.variable("key")
1132+
let valueVar = scope.variable("value")
1133+
printer.write("const [\(keyVar), \(valueVar)] = \(entryVar);")
1134+
1135+
let keyFragment = try! stackLowerFragment(elementType: .string)
1136+
let keyCleanup = CodeFragmentPrinter()
1137+
let _ = keyFragment.printCode([keyVar], scope, printer, keyCleanup)
1138+
if !keyCleanup.lines.isEmpty {
1139+
printer.write("\(cleanupArrayVar).push(() => {")
1140+
printer.indent {
1141+
for line in keyCleanup.lines {
1142+
printer.write(line)
1143+
}
1144+
}
1145+
printer.write("});")
1146+
}
1147+
1148+
let valueFragment = try! stackLowerFragment(elementType: valueType)
1149+
let valueCleanup = CodeFragmentPrinter()
1150+
let _ = valueFragment.printCode([valueVar], scope, printer, valueCleanup)
1151+
if !valueCleanup.lines.isEmpty {
1152+
printer.write("\(cleanupArrayVar).push(() => {")
1153+
printer.indent {
1154+
for line in valueCleanup.lines {
1155+
printer.write(line)
1156+
}
1157+
}
1158+
printer.write("});")
1159+
}
1160+
}
1161+
printer.write("}")
1162+
printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(\(entriesVar).length);")
1163+
cleanupCode.write("for (const cleanup of \(cleanupArrayVar)) { cleanup(); }")
1164+
}
1165+
printer.write("}")
1166+
printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(\(isSomeVar) ? 1 : 0);")
11211167
default:
11221168
()
11231169
}

Sources/JavaScriptKit/BridgeJSIntrinsics.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2334,9 +2334,23 @@ extension Array: _BridgedSwiftStackType where Element: _BridgedSwiftStackType, E
23342334

23352335
// MARK: - Dictionary Support
23362336

2337+
public protocol _BridgedSwiftDictionaryStackType: _BridgedSwiftTypeLoweredIntoVoidType {
2338+
associatedtype DictionaryValue: _BridgedSwiftStackType
2339+
where DictionaryValue.StackLiftResult == DictionaryValue
2340+
}
2341+
23372342
extension Dictionary: _BridgedSwiftStackType
23382343
where Key == String, Value: _BridgedSwiftStackType, Value.StackLiftResult == Value {
23392344
public typealias StackLiftResult = [String: Value]
2345+
// Lowering/return use stack-based encoding, so dictionary also behaves like a void-lowered type.
2346+
// Optional/JSUndefinedOr wrappers rely on this conformance to push an isSome flag and
2347+
// then delegate to the stack-based lowering defined below.
2348+
// swiftlint:disable:next type_name
2349+
}
2350+
2351+
extension Dictionary: _BridgedSwiftTypeLoweredIntoVoidType, _BridgedSwiftDictionaryStackType
2352+
where Key == String, Value: _BridgedSwiftStackType, Value.StackLiftResult == Value {
2353+
public typealias DictionaryValue = Value
23402354

23412355
@_spi(BridgeJS) public static func bridgeJSLiftParameter() -> [String: Value] {
23422356
let count = Int(_swift_js_pop_i32())
@@ -2371,3 +2385,47 @@ where Key == String, Value: _BridgedSwiftStackType, Value.StackLiftResult == Val
23712385
bridgeJSLowerReturn()
23722386
}
23732387
}
2388+
2389+
extension Optional where Wrapped: _BridgedSwiftDictionaryStackType {
2390+
typealias DictionaryValue = Wrapped.DictionaryValue
2391+
2392+
@_spi(BridgeJS) public consuming func bridgeJSLowerParameter() -> Int32 {
2393+
switch consume self {
2394+
case .none:
2395+
return 0
2396+
case .some(let dict):
2397+
dict.bridgeJSLowerReturn()
2398+
return 1
2399+
}
2400+
}
2401+
2402+
@_spi(BridgeJS) public static func bridgeJSLiftReturn() -> [String: Wrapped.DictionaryValue]? {
2403+
let isSome = _swift_js_pop_i32()
2404+
if isSome == 0 {
2405+
return nil
2406+
}
2407+
return Dictionary<String, Wrapped.DictionaryValue>.bridgeJSLiftParameter()
2408+
}
2409+
}
2410+
2411+
extension _BridgedAsOptional where Wrapped: _BridgedSwiftDictionaryStackType {
2412+
typealias DictionaryValue = Wrapped.DictionaryValue
2413+
2414+
@_spi(BridgeJS) public consuming func bridgeJSLowerParameter() -> Int32 {
2415+
let opt = optionalRepresentation
2416+
if let dict = opt {
2417+
dict.bridgeJSLowerReturn()
2418+
return 1
2419+
}
2420+
return 0
2421+
}
2422+
2423+
@_spi(BridgeJS) public static func bridgeJSLiftReturn() -> Self {
2424+
let isSome = _swift_js_pop_i32()
2425+
if isSome == 0 {
2426+
return Self(optional: nil)
2427+
}
2428+
let value = Dictionary<String, Wrapped.DictionaryValue>.bridgeJSLiftParameter() as! Wrapped
2429+
return Self(optional: value)
2430+
}
2431+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit
2+
import XCTest
3+
4+
final class DictionaryTests: XCTestCase {
5+
func testRoundTripDictionary() throws {
6+
let input: [String: Int] = ["a": 1, "b": 2]
7+
let result = try jsRoundTripDictionary(input)
8+
XCTAssertEqual(result, input)
9+
}
10+
11+
func testRoundTripNestedDictionary() throws {
12+
let input: [String: [Double]] = [
13+
"xs": [1.0, 2.5],
14+
"ys": [],
15+
]
16+
let result = try jsRoundTripNestedDictionary(input)
17+
XCTAssertEqual(result, input)
18+
}
19+
20+
func testRoundTripOptionalDictionaryNull() throws {
21+
let some: [String: String]? = ["k": "v"]
22+
XCTAssertEqual(try jsRoundTripOptionalDictionary(some), some)
23+
XCTAssertNil(try jsRoundTripOptionalDictionary(nil))
24+
}
25+
26+
func testRoundTripOptionalDictionaryUndefined() throws {
27+
let some: JSUndefinedOr<[String: Int]> = .value(["n": 42])
28+
let undefined: JSUndefinedOr<[String: Int]> = .undefinedValue
29+
30+
let returnedSome = try jsRoundTripUndefinedDictionary(some)
31+
switch returnedSome {
32+
case .value(let dict):
33+
XCTAssertEqual(dict, ["n": 42])
34+
case .undefined:
35+
XCTFail("Expected defined dictionary")
36+
}
37+
38+
let returnedUndefined = try jsRoundTripUndefinedDictionary(undefined)
39+
switch returnedUndefined {
40+
case .value:
41+
XCTFail("Expected undefined")
42+
case .undefined:
43+
break
44+
}
45+
}
46+
}
47+
48+
@JSFunction func jsRoundTripDictionary(_ values: [String: Int]) throws(JSException) -> [String: Int]
49+
50+
@JSFunction func jsRoundTripNestedDictionary(_ values: [String: [Double]]) throws(JSException) -> [String: [Double]]
51+
52+
@JSFunction func jsRoundTripOptionalDictionary(_ values: [String: String]?) throws(JSException) -> [String: String]?
53+
54+
@JSFunction func jsRoundTripUndefinedDictionary(
55+
_ values: JSUndefinedOr<[String: Int]>
56+
) throws(JSException) -> JSUndefinedOr<[String: Int]>

Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8406,6 +8406,78 @@ fileprivate func _bjs_Container_wrap(_ pointer: UnsafeMutableRawPointer) -> Int3
84068406
}
84078407
#endif
84088408

8409+
#if arch(wasm32)
8410+
@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripDictionary")
8411+
fileprivate func bjs_jsRoundTripDictionary() -> Void
8412+
#else
8413+
fileprivate func bjs_jsRoundTripDictionary() -> Void {
8414+
fatalError("Only available on WebAssembly")
8415+
}
8416+
#endif
8417+
8418+
func _$jsRoundTripDictionary(_ values: [String: Int]) throws(JSException) -> [String: Int] {
8419+
let _ = values.bridgeJSLowerParameter()
8420+
bjs_jsRoundTripDictionary()
8421+
if let error = _swift_js_take_exception() {
8422+
throw error
8423+
}
8424+
return [String: Int].bridgeJSLiftReturn()
8425+
}
8426+
8427+
#if arch(wasm32)
8428+
@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripNestedDictionary")
8429+
fileprivate func bjs_jsRoundTripNestedDictionary() -> Void
8430+
#else
8431+
fileprivate func bjs_jsRoundTripNestedDictionary() -> Void {
8432+
fatalError("Only available on WebAssembly")
8433+
}
8434+
#endif
8435+
8436+
func _$jsRoundTripNestedDictionary(_ values: [String: [Double]]) throws(JSException) -> [String: [Double]] {
8437+
let _ = values.bridgeJSLowerParameter()
8438+
bjs_jsRoundTripNestedDictionary()
8439+
if let error = _swift_js_take_exception() {
8440+
throw error
8441+
}
8442+
return [String: [Double]].bridgeJSLiftReturn()
8443+
}
8444+
8445+
#if arch(wasm32)
8446+
@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripOptionalDictionary")
8447+
fileprivate func bjs_jsRoundTripOptionalDictionary(_ values: Int32) -> Void
8448+
#else
8449+
fileprivate func bjs_jsRoundTripOptionalDictionary(_ values: Int32) -> Void {
8450+
fatalError("Only available on WebAssembly")
8451+
}
8452+
#endif
8453+
8454+
func _$jsRoundTripOptionalDictionary(_ values: Optional<[String: String]>) throws(JSException) -> Optional<[String: String]> {
8455+
let valuesIsSome = values.bridgeJSLowerParameter()
8456+
bjs_jsRoundTripOptionalDictionary(valuesIsSome)
8457+
if let error = _swift_js_take_exception() {
8458+
throw error
8459+
}
8460+
return Optional<[String: String]>.bridgeJSLiftReturn()
8461+
}
8462+
8463+
#if arch(wasm32)
8464+
@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripUndefinedDictionary")
8465+
fileprivate func bjs_jsRoundTripUndefinedDictionary(_ values: Int32) -> Void
8466+
#else
8467+
fileprivate func bjs_jsRoundTripUndefinedDictionary(_ values: Int32) -> Void {
8468+
fatalError("Only available on WebAssembly")
8469+
}
8470+
#endif
8471+
8472+
func _$jsRoundTripUndefinedDictionary(_ values: JSUndefinedOr<[String: Int]>) throws(JSException) -> JSUndefinedOr<[String: Int]> {
8473+
let valuesIsSome = values.bridgeJSLowerParameter()
8474+
bjs_jsRoundTripUndefinedDictionary(valuesIsSome)
8475+
if let error = _swift_js_take_exception() {
8476+
throw error
8477+
}
8478+
return JSUndefinedOr<[String: Int]>.bridgeJSLiftReturn()
8479+
}
8480+
84098481
#if arch(wasm32)
84108482
@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_Foo_init")
84118483
fileprivate func bjs_Foo_init(_ value: Int32) -> Int32

0 commit comments

Comments
 (0)