diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift index 51ff9169..aa01a2c5 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ExportSwift.swift @@ -832,6 +832,8 @@ struct StackCodegen { return liftNullableExpression(wrappedType: wrappedType, kind: kind) case .array(let elementType): return liftArrayExpression(elementType: elementType) + case .dictionary(let valueType): + return liftDictionaryExpression(valueType: valueType) case .closure: return "JSObject.bridgeJSLiftParameter()" case .void, .namespaceEnum: @@ -849,7 +851,7 @@ struct StackCodegen { return liftArrayExpressionInline(elementType: elementType) case .swiftProtocol(let protocolName): return "[Any\(raw: protocolName)].bridgeJSLiftParameter()" - case .nullable, .array, .closure: + case .nullable, .array, .closure, .dictionary: return liftArrayExpressionInline(elementType: elementType) case .void, .namespaceEnum: fatalError("Invalid array element type: \(elementType)") @@ -873,6 +875,51 @@ struct StackCodegen { """ } + func liftDictionaryExpression(valueType: BridgeType) -> ExprSyntax { + switch valueType { + case .int, .uint, .float, .double, .string, .bool, .jsValue, + .jsObject(nil), .swiftStruct, .caseEnum, .swiftHeapObject, + .unsafePointer, .rawValueEnum, .associatedValueEnum: + return "[String: \(raw: valueType.swiftType)].bridgeJSLiftParameter()" + case .jsObject(let className?): + return """ + { + let __dict = [String: JSObject].bridgeJSLiftParameter() + return __dict.mapValues { \(raw: className)(unsafelyWrapping: $0) } + }() + """ + case .swiftProtocol(let protocolName): + return """ + { + let __dict = [String: JSObject].bridgeJSLiftParameter() + return __dict.mapValues { $0 as! Any\(raw: protocolName) } + }() + """ + case .nullable, .array, .dictionary, .closure: + return liftDictionaryExpressionInline(valueType: valueType) + case .void, .namespaceEnum: + fatalError("Invalid dictionary value type: \(valueType)") + } + } + + private func liftDictionaryExpressionInline(valueType: BridgeType) -> ExprSyntax { + let valueLift = liftExpression(for: valueType) + let swiftTypeName = valueType.swiftType + return """ + { + let __count = Int(_swift_js_pop_i32()) + var __result: [String: \(raw: swiftTypeName)] = [:] + __result.reserveCapacity(__count) + for _ in 0..<__count { + let __value = \(valueLift) + let __key = String.bridgeJSLiftParameter() + __result[__key] = __value + } + return __result + }() + """ + } + private func liftNullableExpression(wrappedType: BridgeType, kind: JSOptionalKind) -> ExprSyntax { let typeName = kind == .null ? "Optional" : "JSUndefinedOr" switch wrappedType { @@ -897,6 +944,23 @@ struct StackCodegen { } }() """ + case .dictionary(let valueType): + let dictionaryLift = liftDictionaryExpression(valueType: valueType) + let swiftTypeName = valueType.swiftType + let absentExpr = + kind == .null + ? "\(typeName)<[String: \(swiftTypeName)]>.none" + : "\(typeName)<[String: \(swiftTypeName)]>.undefinedValue" + return """ + { + let __isSome = _swift_js_pop_i32() + if __isSome == 0 { + return \(raw: absentExpr) + } else { + return \(dictionaryLift) + } + }() + """ case .nullable, .void, .namespaceEnum, .closure, .unsafePointer, .swiftProtocol: fatalError("Invalid nullable wrapped type: \(wrappedType)") } @@ -935,6 +999,8 @@ struct StackCodegen { return [] case .array(let elementType): return lowerArrayStatements(elementType: elementType, accessor: accessor, varPrefix: varPrefix) + case .dictionary(let valueType): + return lowerDictionaryStatements(valueType: valueType, accessor: accessor, varPrefix: varPrefix) } } @@ -952,7 +1018,7 @@ struct StackCodegen { return ["\(raw: accessor).map { $0.jsObject }.bridgeJSLowerReturn()"] case .swiftProtocol(let protocolName): return ["\(raw: accessor).map { $0 as! Any\(raw: protocolName) }.bridgeJSLowerReturn()"] - case .nullable, .array, .closure: + case .nullable, .array, .closure, .dictionary: return lowerArrayStatementsInline( elementType: elementType, accessor: accessor, @@ -986,6 +1052,65 @@ struct StackCodegen { return statements } + private func lowerDictionaryStatements( + valueType: BridgeType, + accessor: String, + varPrefix: String + ) -> [CodeBlockItemSyntax] { + switch valueType { + case .int, .uint, .float, .double, .string, .bool, .jsValue, + .jsObject(nil), .swiftStruct, .caseEnum, .swiftHeapObject, + .unsafePointer, .rawValueEnum, .associatedValueEnum: + return ["\(raw: accessor).bridgeJSLowerReturn()"] + case .jsObject(_?): + return ["\(raw: accessor).mapValues { $0.jsObject }.bridgeJSLowerReturn()"] + case .swiftProtocol(let protocolName): + return ["\(raw: accessor).mapValues { $0 as! Any\(raw: protocolName) }.bridgeJSLowerReturn()"] + case .nullable, .array, .dictionary, .closure: + return lowerDictionaryStatementsInline( + valueType: valueType, + accessor: accessor, + varPrefix: varPrefix + ) + case .void, .namespaceEnum: + fatalError("Invalid dictionary value type: \(valueType)") + } + } + + private func lowerDictionaryStatementsInline( + valueType: BridgeType, + accessor: String, + varPrefix: String + ) -> [CodeBlockItemSyntax] { + var statements: [CodeBlockItemSyntax] = [] + let pairVarName = "__bjs_kv_\(varPrefix)" + statements.append("for \(raw: pairVarName) in \(raw: accessor) {") + statements.append("let __bjs_key_\(raw: varPrefix) = \(raw: pairVarName).key") + statements.append("let __bjs_value_\(raw: varPrefix) = \(raw: pairVarName).value") + + let keyStatements = lowerStatements( + for: .string, + accessor: "__bjs_key_\(varPrefix)", + varPrefix: "\(varPrefix)_key" + ) + for stmt in keyStatements { + statements.append(stmt) + } + + let valueStatements = lowerStatements( + for: valueType, + accessor: "__bjs_value_\(varPrefix)", + varPrefix: "\(varPrefix)_value" + ) + for stmt in valueStatements { + statements.append(stmt) + } + + statements.append("}") + statements.append("_swift_js_push_i32(Int32(\(raw: accessor).count))") + return statements + } + private func lowerOptionalStatements( wrappedType: BridgeType, accessor: String, @@ -1033,6 +1158,8 @@ struct StackCodegen { return ["\(raw: unwrappedVar).jsObject.bridgeJSLowerStackReturn()"] case .array(let elementType): return lowerArrayStatements(elementType: elementType, accessor: unwrappedVar, varPrefix: varPrefix) + case .dictionary(let valueType): + return lowerDictionaryStatements(valueType: valueType, accessor: unwrappedVar, varPrefix: varPrefix) default: return ["preconditionFailure(\"BridgeJS: unsupported optional wrapped type\")"] } @@ -1616,6 +1743,7 @@ extension BridgeType { case .nullable(let wrappedType, let kind): return kind == .null ? "Optional<\(wrappedType.swiftType)>" : "JSUndefinedOr<\(wrappedType.swiftType)>" case .array(let elementType): return "[\(elementType.swiftType)]" + case .dictionary(let valueType): return "[String: \(valueType.swiftType)]" case .caseEnum(let name): return name case .rawValueEnum(let name, _): return name case .associatedValueEnum(let name): return name @@ -1675,7 +1803,7 @@ extension BridgeType { throw BridgeJSCoreError("Namespace enums are not supported to pass as parameters") case .closure: return LiftingIntrinsicInfo(parameters: [("callbackId", .i32)]) - case .array: + case .array, .dictionary: return LiftingIntrinsicInfo(parameters: []) } } @@ -1726,7 +1854,7 @@ extension BridgeType { throw BridgeJSCoreError("Namespace enums are not supported to pass as parameters") case .closure: return .swiftHeapObject - case .array: + case .array, .dictionary: return .array } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift index 8db872c6..7c3ecd6e 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/ImportTS.swift @@ -980,7 +980,7 @@ extension BridgeType { var params = [("isSome", WasmCoreType.i32)] params.append(contentsOf: wrappedInfo.loweredParameters) return LoweringParameterInfo(loweredParameters: params) - case .array: + case .array, .dictionary: return LoweringParameterInfo(loweredParameters: []) } } @@ -1059,7 +1059,7 @@ extension BridgeType { case .nullable(let wrappedType, _): let wrappedInfo = try wrappedType.liftingReturnInfo(context: context) return LiftingReturnInfo(valueToLift: wrappedInfo.valueToLift) - case .array: + case .array, .dictionary: return LiftingReturnInfo(valueToLift: nil) } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift index 3de74f74..e571d955 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift @@ -213,6 +213,45 @@ public final class SwiftToSkeleton { return .array(elementType) } } + // [String: T] + if let dictType = type.as(DictionaryTypeSyntax.self) { + if let keyType = lookupType(for: dictType.key, errors: &errors), + keyType == .string, + let valueType = lookupType(for: dictType.value, errors: &errors) + { + return .dictionary(valueType) + } + } + // Dictionary + if let identifierType = type.as(IdentifierTypeSyntax.self), + identifierType.name.text == "Dictionary", + let genericArgs = identifierType.genericArgumentClause?.arguments, + genericArgs.count == 2, + let keyArg = TypeSyntax(genericArgs.first?.argument), + let valueArg = TypeSyntax(genericArgs.last?.argument), + let keyType = lookupType(for: keyArg, errors: &errors), + keyType == .string + { + if let valueType = lookupType(for: valueArg, errors: &errors) { + return .dictionary(valueType) + } + } + // Swift.Dictionary + if let memberType = type.as(MemberTypeSyntax.self), + let baseType = memberType.baseType.as(IdentifierTypeSyntax.self), + baseType.name.text == "Swift", + memberType.name.text == "Dictionary", + let genericArgs = memberType.genericArgumentClause?.arguments, + genericArgs.count == 2, + let keyArg = TypeSyntax(genericArgs.first?.argument), + let valueArg = TypeSyntax(genericArgs.last?.argument), + let keyType = lookupType(for: keyArg, errors: &errors), + keyType == .string + { + if let valueType = lookupType(for: valueArg, errors: &errors) { + return .dictionary(valueType) + } + } if let aliasDecl = typeDeclResolver.resolveTypeAlias(type) { if let resolvedType = lookupType(for: aliasDecl.initializer.value, errors: &errors) { return resolvedType diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index c2b0c6ed..9303962a 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -1462,6 +1462,9 @@ public struct BridgeJSLink { return "(\(elementTypeStr))[]" } return "\(elementTypeStr)[]" + case .dictionary(let valueType): + let valueTypeStr = resolveTypeScriptType(valueType, exportedSkeletons: exportedSkeletons) + return "Record" default: return type.tsType } @@ -3564,6 +3567,8 @@ extension BridgeType { return "(\(inner))[]" } return "\(inner)[]" + case .dictionary(let valueType): + return "Record" } } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift index ae83f33e..0b170d5d 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift @@ -737,6 +737,23 @@ struct IntrinsicJSFragment: Sendable { } printer.write("}") resultExpr = arrayVar + case .dictionary(let valueType): + let dictVar = scope.variable("dictValue") + printer.write("let \(dictVar);") + printer.write("if (\(isSome)) {") + printer.indent { + let dictLiftFragment = try! dictionaryLift(valueType: valueType) + let liftResults = dictLiftFragment.printCode([], scope, printer, cleanupCode) + if let liftResult = liftResults.first { + printer.write("\(dictVar) = \(liftResult);") + } + } + printer.write("} else {") + printer.indent { + printer.write("\(dictVar) = \(absenceLiteral);") + } + printer.write("}") + resultExpr = dictVar default: resultExpr = "\(isSome) ? \(wrappedValue) : \(absenceLiteral)" } @@ -830,6 +847,23 @@ struct IntrinsicJSFragment: Sendable { printer.write("}") cleanupCode.write("for (const cleanup of \(cleanupArrayVar)) { cleanup(); }") return ["+\(isSomeVar)"] + case .dictionary(let valueType): + let cleanupArrayVar = scope.variable("\(value)Cleanups") + printer.write("const \(cleanupArrayVar) = [];") + printer.write("if (\(isSomeVar)) {") + printer.indent { + let dictLowerFragment = try! dictionaryLower(valueType: valueType) + let dictCleanup = CodeFragmentPrinter() + let _ = dictLowerFragment.printCode([value], scope, printer, dictCleanup) + if !dictCleanup.lines.isEmpty { + for line in dictCleanup.lines { + printer.write("\(cleanupArrayVar).push(() => { \(line) });") + } + } + } + printer.write("}") + cleanupCode.write("for (const cleanup of \(cleanupArrayVar)) { cleanup(); }") + return ["+\(isSomeVar)"] default: switch wrappedType { case .swiftHeapObject: @@ -981,6 +1015,23 @@ struct IntrinsicJSFragment: Sendable { printer.write("\(resultVar) = \(absenceLiteral);") } printer.write("}") + case .dictionary(let valueType): + let isSomeVar = scope.variable("isSome") + printer.write("const \(isSomeVar) = \(JSGlueVariableScope.reservedTmpRetInts).pop();") + printer.write("let \(resultVar);") + printer.write("if (\(isSomeVar)) {") + printer.indent { + let dictLiftFragment = try! dictionaryLift(valueType: valueType) + let liftResults = dictLiftFragment.printCode([], scope, printer, cleanupCode) + if let liftResult = liftResults.first { + printer.write("\(resultVar) = \(liftResult);") + } + } + printer.write("} else {") + printer.indent { + printer.write("\(resultVar) = \(absenceLiteral);") + } + printer.write("}") case .jsValue: let isSomeVar = scope.variable("isSome") printer.write("const \(isSomeVar) = \(scope.popI32Return());") @@ -1133,6 +1184,52 @@ struct IntrinsicJSFragment: Sendable { } printer.write("}") cleanupCode.write("if (\(cleanupVar)) { \(cleanupVar)(); }") + case .dictionary(let valueType): + printer.write("if (\(isSomeVar)) {") + printer.indent { + let cleanupArrayVar = scope.variable("arrayCleanups") + let entriesVar = scope.variable("entries") + let entryVar = scope.variable("entry") + printer.write("const \(cleanupArrayVar) = [];") + printer.write("const \(entriesVar) = Object.entries(\(value));") + printer.write("for (const \(entryVar) of \(entriesVar)) {") + printer.indent { + let keyVar = scope.variable("key") + let valueVar = scope.variable("value") + printer.write("const [\(keyVar), \(valueVar)] = \(entryVar);") + + let keyFragment = try! stackLowerFragment(elementType: .string) + let keyCleanup = CodeFragmentPrinter() + let _ = keyFragment.printCode([keyVar], scope, printer, keyCleanup) + if !keyCleanup.lines.isEmpty { + printer.write("\(cleanupArrayVar).push(() => {") + printer.indent { + for line in keyCleanup.lines { + printer.write(line) + } + } + printer.write("});") + } + + let valueFragment = try! stackLowerFragment(elementType: valueType) + let valueCleanup = CodeFragmentPrinter() + let _ = valueFragment.printCode([valueVar], scope, printer, valueCleanup) + if !valueCleanup.lines.isEmpty { + printer.write("\(cleanupArrayVar).push(() => {") + printer.indent { + for line in valueCleanup.lines { + printer.write(line) + } + } + printer.write("});") + } + } + printer.write("}") + printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(\(entriesVar).length);") + cleanupCode.write("for (const cleanup of \(cleanupArrayVar)) { cleanup(); }") + } + printer.write("}") + printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(\(isSomeVar) ? 1 : 0);") default: () } @@ -1807,6 +1904,8 @@ struct IntrinsicJSFragment: Sendable { throw BridgeJSLinkError(message: "Namespace enums are not supported to be passed as parameters: \(string)") case .array(let elementType): return try arrayLower(elementType: elementType) + case .dictionary(let valueType): + return try dictionaryLower(valueType: valueType) } } @@ -1854,6 +1953,8 @@ struct IntrinsicJSFragment: Sendable { ) case .array(let elementType): return try arrayLift(elementType: elementType) + case .dictionary(let valueType): + return try dictionaryLift(valueType: valueType) } } @@ -1945,6 +2046,8 @@ struct IntrinsicJSFragment: Sendable { ) case .array(let elementType): return try arrayLift(elementType: elementType) + case .dictionary(let valueType): + return try dictionaryLift(valueType: valueType) } } @@ -2014,6 +2117,8 @@ struct IntrinsicJSFragment: Sendable { ) case .array(let elementType): return try arrayLower(elementType: elementType) + case .dictionary(let valueType): + return try dictionaryLower(valueType: valueType) } } @@ -2607,6 +2712,58 @@ struct IntrinsicJSFragment: Sendable { ) } + /// Lowers a dictionary from JS to Swift by iterating entries and pushing to stacks + static func dictionaryLower(valueType: BridgeType) throws -> IntrinsicJSFragment { + return IntrinsicJSFragment( + parameters: ["dict"], + printCode: { arguments, scope, printer, cleanupCode in + let dict = arguments[0] + let cleanupArrayVar = scope.variable("arrayCleanups") + + printer.write("const \(cleanupArrayVar) = [];") + let entriesVar = scope.variable("entries") + let entryVar = scope.variable("entry") + printer.write("const \(entriesVar) = Object.entries(\(dict));") + printer.write("for (const \(entryVar) of \(entriesVar)) {") + printer.indent { + let keyVar = scope.variable("key") + let valueVar = scope.variable("value") + printer.write("const [\(keyVar), \(valueVar)] = \(entryVar);") + + let keyFragment = try! stackLowerFragment(elementType: .string) + let keyCleanup = CodeFragmentPrinter() + let _ = keyFragment.printCode([keyVar], scope, printer, keyCleanup) + if !keyCleanup.lines.isEmpty { + printer.write("\(cleanupArrayVar).push(() => {") + printer.indent { + for line in keyCleanup.lines { + printer.write(line) + } + } + printer.write("});") + } + + let valueFragment = try! stackLowerFragment(elementType: valueType) + let valueCleanup = CodeFragmentPrinter() + let _ = valueFragment.printCode([valueVar], scope, printer, valueCleanup) + if !valueCleanup.lines.isEmpty { + printer.write("\(cleanupArrayVar).push(() => {") + printer.indent { + for line in valueCleanup.lines { + printer.write(line) + } + } + printer.write("});") + } + } + printer.write("}") + printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(\(entriesVar).length);") + cleanupCode.write("for (const cleanup of \(cleanupArrayVar)) { cleanup(); }") + return [] + } + ) + } + /// Lifts an array from Swift to JS by popping elements from stacks static func arrayLift(elementType: BridgeType) throws -> IntrinsicJSFragment { return IntrinsicJSFragment( @@ -2633,13 +2790,39 @@ struct IntrinsicJSFragment: Sendable { ) } + /// Lifts a dictionary from Swift to JS by popping key/value pairs from stacks + static func dictionaryLift(valueType: BridgeType) throws -> IntrinsicJSFragment { + return IntrinsicJSFragment( + parameters: [], + printCode: { arguments, scope, printer, cleanupCode in + let resultVar = scope.variable("dictResult") + let lenVar = scope.variable("dictLen") + let iVar = scope.variable("i") + + printer.write("const \(lenVar) = \(JSGlueVariableScope.reservedTmpRetInts).pop();") + printer.write("const \(resultVar) = {};") + printer.write("for (let \(iVar) = 0; \(iVar) < \(lenVar); \(iVar)++) {") + printer.indent { + let valueFragment = try! stackLiftFragment(elementType: valueType) + let valueResults = valueFragment.printCode([], scope, printer, cleanupCode) + let keyFragment = try! stackLiftFragment(elementType: .string) + let keyResults = keyFragment.printCode([], scope, printer, cleanupCode) + if let keyExpr = keyResults.first, let valueExpr = valueResults.first { + printer.write("\(resultVar)[\(keyExpr)] = \(valueExpr);") + } + } + printer.write("}") + return [resultVar] + } + ) + } + private static func stackLiftFragment(elementType: BridgeType) throws -> IntrinsicJSFragment { switch elementType { case .jsValue: return IntrinsicJSFragment( parameters: [], printCode: { _, scope, printer, cleanup in - registerJSValueHelpers(scope: scope) let payload2Var = scope.variable("jsValuePayload2") let payload1Var = scope.variable("jsValuePayload1") let kindVar = scope.variable("jsValueKind") @@ -2647,6 +2830,7 @@ struct IntrinsicJSFragment: Sendable { printer.write("const \(payload1Var) = \(scope.popI32Return());") printer.write("const \(kindVar) = \(scope.popI32Return());") let resultVar = scope.variable("jsValue") + registerJSValueHelpers(scope: scope) printer.write( "const \(resultVar) = \(jsValueLiftHelperName)(\(kindVar), \(payload1Var), \(payload2Var));" ) @@ -2795,6 +2979,8 @@ struct IntrinsicJSFragment: Sendable { ) case .array(let innerElementType): return try! arrayLift(elementType: innerElementType) + case .dictionary(let valueType): + return try! dictionaryLift(valueType: valueType) case .nullable(let wrappedType, let kind): return try optionalElementRaiseFragment(wrappedType: wrappedType, kind: kind) case .unsafePointer: @@ -2980,6 +3166,8 @@ struct IntrinsicJSFragment: Sendable { ) case .array(let innerElementType): return try! arrayLower(elementType: innerElementType) + case .dictionary(let valueType): + return try! dictionaryLower(valueType: valueType) case .nullable(let wrappedType, let kind): return try optionalElementLowerFragment( wrappedType: wrappedType, diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index 0d4a78d3..3029fe0c 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -153,6 +153,7 @@ public enum BridgeType: Codable, Equatable, Hashable, Sendable { case unsafePointer(UnsafePointerType) indirect case nullable(BridgeType, JSOptionalKind) indirect case array(BridgeType) + indirect case dictionary(BridgeType) case caseEnum(String) case rawValueEnum(String, SwiftEnumRawType) case associatedValueEnum(String) @@ -959,6 +960,9 @@ extension BridgeType { case .array: // Arrays use stack-based return with length prefix (no direct WASM return type) return nil + case .dictionary: + // Dictionaries use stack-based return with entry count (no direct WASM return type) + return nil } } @@ -1024,6 +1028,9 @@ extension BridgeType { case .array(let elementType): // Array mangling: "Sa" prefix followed by element type return "Sa\(elementType.mangleTypeName)" + case .dictionary(let valueType): + // Dictionary mangling: "SD" prefix followed by value type (key is always String) + return "SD\(valueType.mangleTypeName)" } } diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/DictionaryTypes.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/DictionaryTypes.swift new file mode 100644 index 00000000..c699ea79 --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/DictionaryTypes.swift @@ -0,0 +1,15 @@ +@JS class Box { + var value: Int + + init(value: Int) { + self.value = value + } +} + +@JS func mirrorDictionary(_ values: [String: Int]) -> [String: Int] +@JS func optionalDictionary(_ values: [String: String]?) -> [String: String]? +@JS func nestedDictionary(_ values: [String: [Int]]) -> [String: [Int]] +@JS func boxDictionary(_ boxes: [String: Box]) -> [String: Box] +@JS func optionalBoxDictionary(_ boxes: [String: Box?]) -> [String: Box?] + +@JSFunction func importMirrorDictionary(_ values: [String: Double]) throws(JSException) -> [String: Double] diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DictionaryTypes.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DictionaryTypes.json new file mode 100644 index 00000000..af5b83dc --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DictionaryTypes.json @@ -0,0 +1,259 @@ +{ + "exported" : { + "classes" : [ + { + "methods" : [ + + ], + "name" : "Box", + "properties" : [ + + ], + "swiftCallName" : "Box" + } + ], + "enums" : [ + + ], + "exposeToGlobal" : false, + "functions" : [ + { + "abiName" : "bjs_mirrorDictionary", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "mirrorDictionary", + "parameters" : [ + { + "label" : "_", + "name" : "values", + "type" : { + "dictionary" : { + "_0" : { + "int" : { + + } + } + } + } + } + ], + "returnType" : { + "dictionary" : { + "_0" : { + "int" : { + + } + } + } + } + }, + { + "abiName" : "bjs_optionalDictionary", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "optionalDictionary", + "parameters" : [ + { + "label" : "_", + "name" : "values", + "type" : { + "nullable" : { + "_0" : { + "dictionary" : { + "_0" : { + "string" : { + + } + } + } + }, + "_1" : "null" + } + } + } + ], + "returnType" : { + "nullable" : { + "_0" : { + "dictionary" : { + "_0" : { + "string" : { + + } + } + } + }, + "_1" : "null" + } + } + }, + { + "abiName" : "bjs_nestedDictionary", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "nestedDictionary", + "parameters" : [ + { + "label" : "_", + "name" : "values", + "type" : { + "dictionary" : { + "_0" : { + "array" : { + "_0" : { + "int" : { + + } + } + } + } + } + } + } + ], + "returnType" : { + "dictionary" : { + "_0" : { + "array" : { + "_0" : { + "int" : { + + } + } + } + } + } + } + }, + { + "abiName" : "bjs_boxDictionary", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "boxDictionary", + "parameters" : [ + { + "label" : "_", + "name" : "boxes", + "type" : { + "dictionary" : { + "_0" : { + "swiftHeapObject" : { + "_0" : "Box" + } + } + } + } + } + ], + "returnType" : { + "dictionary" : { + "_0" : { + "swiftHeapObject" : { + "_0" : "Box" + } + } + } + } + }, + { + "abiName" : "bjs_optionalBoxDictionary", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "optionalBoxDictionary", + "parameters" : [ + { + "label" : "_", + "name" : "boxes", + "type" : { + "dictionary" : { + "_0" : { + "nullable" : { + "_0" : { + "swiftHeapObject" : { + "_0" : "Box" + } + }, + "_1" : "null" + } + } + } + } + } + ], + "returnType" : { + "dictionary" : { + "_0" : { + "nullable" : { + "_0" : { + "swiftHeapObject" : { + "_0" : "Box" + } + }, + "_1" : "null" + } + } + } + } + } + ], + "protocols" : [ + + ], + "structs" : [ + + ] + }, + "imported" : { + "children" : [ + { + "functions" : [ + { + "name" : "importMirrorDictionary", + "parameters" : [ + { + "name" : "values", + "type" : { + "dictionary" : { + "_0" : { + "double" : { + + } + } + } + } + } + ], + "returnType" : { + "dictionary" : { + "_0" : { + "double" : { + + } + } + } + } + } + ], + "types" : [ + + ] + } + ] + }, + "moduleName" : "TestModule" +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DictionaryTypes.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DictionaryTypes.swift new file mode 100644 index 00000000..2a41c35a --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/DictionaryTypes.swift @@ -0,0 +1,97 @@ +@_expose(wasm, "bjs_mirrorDictionary") +@_cdecl("bjs_mirrorDictionary") +public func _bjs_mirrorDictionary() -> Void { + #if arch(wasm32) + let ret = mirrorDictionary(_: [String: Int].bridgeJSLiftParameter()) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_optionalDictionary") +@_cdecl("bjs_optionalDictionary") +public func _bjs_optionalDictionary(_ values: Int32) -> Void { + #if arch(wasm32) + let ret = optionalDictionary(_: Optional<[String: String]>.bridgeJSLiftParameter(values)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_nestedDictionary") +@_cdecl("bjs_nestedDictionary") +public func _bjs_nestedDictionary() -> Void { + #if arch(wasm32) + let ret = nestedDictionary(_: [String: [Int]].bridgeJSLiftParameter()) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_boxDictionary") +@_cdecl("bjs_boxDictionary") +public func _bjs_boxDictionary() -> Void { + #if arch(wasm32) + let ret = boxDictionary(_: [String: Box].bridgeJSLiftParameter()) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_optionalBoxDictionary") +@_cdecl("bjs_optionalBoxDictionary") +public func _bjs_optionalBoxDictionary() -> Void { + #if arch(wasm32) + let ret = optionalBoxDictionary(_: [String: Optional].bridgeJSLiftParameter()) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_Box_deinit") +@_cdecl("bjs_Box_deinit") +public func _bjs_Box_deinit(_ pointer: UnsafeMutableRawPointer) -> Void { + #if arch(wasm32) + Unmanaged.fromOpaque(pointer).release() + #else + fatalError("Only available on WebAssembly") + #endif +} + +extension Box: ConvertibleToJSValue, _BridgedSwiftHeapObject { + var jsValue: JSValue { + return .object(JSObject(id: UInt32(bitPattern: _bjs_Box_wrap(Unmanaged.passRetained(self).toOpaque())))) + } +} + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_Box_wrap") +fileprivate func _bjs_Box_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 +#else +fileprivate func _bjs_Box_wrap(_ pointer: UnsafeMutableRawPointer) -> Int32 { + fatalError("Only available on WebAssembly") +} +#endif + +#if arch(wasm32) +@_extern(wasm, module: "TestModule", name: "bjs_importMirrorDictionary") +fileprivate func bjs_importMirrorDictionary() -> Void +#else +fileprivate func bjs_importMirrorDictionary() -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$importMirrorDictionary(_ values: [String: Double]) throws(JSException) -> [String: Double] { + let _ = values.bridgeJSLowerParameter() + bjs_importMirrorDictionary() + if let error = _swift_js_take_exception() { + throw error + } + return [String: Double].bridgeJSLiftReturn() +} \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DictionaryTypes.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DictionaryTypes.d.ts new file mode 100644 index 00000000..dadcc74b --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DictionaryTypes.d.ts @@ -0,0 +1,34 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +/// Represents a Swift heap object like a class instance or an actor instance. +export interface SwiftHeapObject { + /// Release the heap object. + /// + /// Note: Calling this method will release the heap object and it will no longer be accessible. + release(): void; +} +export interface Box extends SwiftHeapObject { +} +export type Exports = { + Box: { + } + mirrorDictionary(values: Record): Record; + optionalDictionary(values: Record | null): Record | null; + nestedDictionary(values: Record): Record; + boxDictionary(boxes: Record): Record; + optionalBoxDictionary(boxes: Record): Record; +} +export type Imports = { + importMirrorDictionary(values: Record): Record; +} +export function createInstantiator(options: { + imports: Imports; +}, swift: any): Promise<{ + addImports: (importObject: WebAssembly.Imports) => void; + setInstance: (instance: WebAssembly.Instance) => void; + createExports: (instance: WebAssembly.Instance) => Exports; +}>; \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DictionaryTypes.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DictionaryTypes.js new file mode 100644 index 00000000..0388f7de --- /dev/null +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/DictionaryTypes.js @@ -0,0 +1,462 @@ +// NOTICE: This is auto-generated code by BridgeJS from JavaScriptKit, +// DO NOT EDIT. +// +// To update this file, just rebuild your project or run +// `swift package bridge-js`. + +export async function createInstantiator(options, swift) { + let instance; + let memory; + let setException; + const textDecoder = new TextDecoder("utf-8"); + const textEncoder = new TextEncoder("utf-8"); + let tmpRetString; + let tmpRetBytes; + let tmpRetException; + let tmpRetOptionalBool; + let tmpRetOptionalInt; + let tmpRetOptionalFloat; + let tmpRetOptionalDouble; + let tmpRetOptionalHeapObject; + let tmpRetTag = []; + let tmpRetStrings = []; + let tmpRetInts = []; + let tmpRetF32s = []; + let tmpRetF64s = []; + let tmpParamInts = []; + let tmpParamF32s = []; + let tmpParamF64s = []; + let tmpRetPointers = []; + let tmpParamPointers = []; + let tmpStructCleanups = []; + const enumHelpers = {}; + const structHelpers = {}; + + let _exports = null; + let bjs = null; + + return { + /** + * @param {WebAssembly.Imports} importObject + */ + addImports: (importObject, importsContext) => { + bjs = {}; + importObject["bjs"] = bjs; + const imports = options.getImports(importsContext); + bjs["swift_js_return_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + tmpRetString = textDecoder.decode(bytes); + } + bjs["swift_js_init_memory"] = function(sourceId, bytesPtr) { + const source = swift.memory.getObject(sourceId); + const bytes = new Uint8Array(memory.buffer, bytesPtr); + bytes.set(source); + } + bjs["swift_js_make_js_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + return swift.memory.retain(textDecoder.decode(bytes)); + } + bjs["swift_js_init_memory_with_result"] = function(ptr, len) { + const target = new Uint8Array(memory.buffer, ptr, len); + target.set(tmpRetBytes); + tmpRetBytes = undefined; + } + bjs["swift_js_throw"] = function(id) { + tmpRetException = swift.memory.retainByRef(id); + } + bjs["swift_js_retain"] = function(id) { + return swift.memory.retainByRef(id); + } + bjs["swift_js_release"] = function(id) { + swift.memory.release(id); + } + bjs["swift_js_push_tag"] = function(tag) { + tmpRetTag.push(tag); + } + bjs["swift_js_push_i32"] = function(v) { + tmpRetInts.push(v | 0); + } + bjs["swift_js_push_f32"] = function(v) { + tmpRetF32s.push(Math.fround(v)); + } + bjs["swift_js_push_f64"] = function(v) { + tmpRetF64s.push(v); + } + bjs["swift_js_push_string"] = function(ptr, len) { + const bytes = new Uint8Array(memory.buffer, ptr, len); + const value = textDecoder.decode(bytes); + tmpRetStrings.push(value); + } + bjs["swift_js_pop_i32"] = function() { + return tmpParamInts.pop(); + } + bjs["swift_js_pop_f32"] = function() { + return tmpParamF32s.pop(); + } + bjs["swift_js_pop_f64"] = function() { + return tmpParamF64s.pop(); + } + bjs["swift_js_push_pointer"] = function(pointer) { + tmpRetPointers.push(pointer); + } + bjs["swift_js_pop_pointer"] = function() { + return tmpParamPointers.pop(); + } + bjs["swift_js_struct_cleanup"] = function(cleanupId) { + if (cleanupId === 0) { return; } + const index = (cleanupId | 0) - 1; + const cleanup = tmpStructCleanups[index]; + tmpStructCleanups[index] = null; + if (cleanup) { cleanup(); } + while (tmpStructCleanups.length > 0 && tmpStructCleanups[tmpStructCleanups.length - 1] == null) { + tmpStructCleanups.pop(); + } + } + bjs["swift_js_return_optional_bool"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalBool = null; + } else { + tmpRetOptionalBool = value !== 0; + } + } + bjs["swift_js_return_optional_int"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalInt = null; + } else { + tmpRetOptionalInt = value | 0; + } + } + bjs["swift_js_return_optional_float"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalFloat = null; + } else { + tmpRetOptionalFloat = Math.fround(value); + } + } + bjs["swift_js_return_optional_double"] = function(isSome, value) { + if (isSome === 0) { + tmpRetOptionalDouble = null; + } else { + tmpRetOptionalDouble = value; + } + } + bjs["swift_js_return_optional_string"] = function(isSome, ptr, len) { + if (isSome === 0) { + tmpRetString = null; + } else { + const bytes = new Uint8Array(memory.buffer, ptr, len); + tmpRetString = textDecoder.decode(bytes); + } + } + bjs["swift_js_return_optional_object"] = function(isSome, objectId) { + if (isSome === 0) { + tmpRetString = null; + } else { + tmpRetString = swift.memory.getObject(objectId); + } + } + bjs["swift_js_return_optional_heap_object"] = function(isSome, pointer) { + if (isSome === 0) { + tmpRetOptionalHeapObject = null; + } else { + tmpRetOptionalHeapObject = pointer; + } + } + bjs["swift_js_get_optional_int_presence"] = function() { + return tmpRetOptionalInt != null ? 1 : 0; + } + bjs["swift_js_get_optional_int_value"] = function() { + const value = tmpRetOptionalInt; + tmpRetOptionalInt = undefined; + return value; + } + bjs["swift_js_get_optional_string"] = function() { + const str = tmpRetString; + tmpRetString = undefined; + if (str == null) { + return -1; + } else { + const bytes = textEncoder.encode(str); + tmpRetBytes = bytes; + return bytes.length; + } + } + bjs["swift_js_get_optional_float_presence"] = function() { + return tmpRetOptionalFloat != null ? 1 : 0; + } + bjs["swift_js_get_optional_float_value"] = function() { + const value = tmpRetOptionalFloat; + tmpRetOptionalFloat = undefined; + return value; + } + bjs["swift_js_get_optional_double_presence"] = function() { + return tmpRetOptionalDouble != null ? 1 : 0; + } + bjs["swift_js_get_optional_double_value"] = function() { + const value = tmpRetOptionalDouble; + tmpRetOptionalDouble = undefined; + return value; + } + bjs["swift_js_get_optional_heap_object_pointer"] = function() { + const pointer = tmpRetOptionalHeapObject; + tmpRetOptionalHeapObject = undefined; + return pointer || 0; + } + // Wrapper functions for module: TestModule + if (!importObject["TestModule"]) { + importObject["TestModule"] = {}; + } + importObject["TestModule"]["bjs_Box_wrap"] = function(pointer) { + const obj = Box.__construct(pointer); + return swift.memory.retain(obj); + }; + const TestModule = importObject["TestModule"] = importObject["TestModule"] || {}; + TestModule["bjs_importMirrorDictionary"] = function bjs_importMirrorDictionary() { + try { + const dictLen = tmpRetInts.pop(); + const dictResult = {}; + for (let i = 0; i < dictLen; i++) { + const f64 = tmpRetF64s.pop(); + const string = tmpRetStrings.pop(); + dictResult[string] = f64; + } + let ret = imports.importMirrorDictionary(dictResult); + const arrayCleanups = []; + const entries = Object.entries(ret); + for (const entry of entries) { + const [key, value] = entry; + const bytes = textEncoder.encode(key); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); + arrayCleanups.push(() => { + swift.memory.release(id); + }); + tmpParamF64s.push(value); + } + tmpParamInts.push(entries.length); + } catch (error) { + setException(error); + } + } + }, + setInstance: (i) => { + instance = i; + memory = instance.exports.memory; + + setException = (error) => { + instance.exports._swift_js_exception.value = swift.memory.retain(error) + } + }, + /** @param {WebAssembly.Instance} instance */ + createExports: (instance) => { + const js = swift.memory.heap; + /// Represents a Swift heap object like a class instance or an actor instance. + class SwiftHeapObject { + static __wrap(pointer, deinit, prototype) { + const obj = Object.create(prototype); + obj.pointer = pointer; + obj.hasReleased = false; + obj.deinit = deinit; + obj.registry = new FinalizationRegistry((pointer) => { + deinit(pointer); + }); + obj.registry.register(this, obj.pointer); + return obj; + } + + release() { + this.registry.unregister(this); + this.deinit(this.pointer); + } + } + class Box extends SwiftHeapObject { + static __construct(ptr) { + return SwiftHeapObject.__wrap(ptr, instance.exports.bjs_Box_deinit, Box.prototype); + } + + } + const exports = { + Box, + mirrorDictionary: function bjs_mirrorDictionary(values) { + const arrayCleanups = []; + const entries = Object.entries(values); + for (const entry of entries) { + const [key, value] = entry; + const bytes = textEncoder.encode(key); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); + arrayCleanups.push(() => { + swift.memory.release(id); + }); + tmpParamInts.push((value | 0)); + } + tmpParamInts.push(entries.length); + instance.exports.bjs_mirrorDictionary(); + const dictLen = tmpRetInts.pop(); + const dictResult = {}; + for (let i = 0; i < dictLen; i++) { + const int = tmpRetInts.pop(); + const string = tmpRetStrings.pop(); + dictResult[string] = int; + } + for (const cleanup of arrayCleanups) { cleanup(); } + return dictResult; + }, + optionalDictionary: function bjs_optionalDictionary(values) { + const isSome = values != null; + const valuesCleanups = []; + if (isSome) { + const arrayCleanups = []; + const entries = Object.entries(values); + for (const entry of entries) { + const [key, value] = entry; + const bytes = textEncoder.encode(key); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); + arrayCleanups.push(() => { + swift.memory.release(id); + }); + const bytes1 = textEncoder.encode(value); + const id1 = swift.memory.retain(bytes1); + tmpParamInts.push(bytes1.length); + tmpParamInts.push(id1); + arrayCleanups.push(() => { + swift.memory.release(id1); + }); + } + tmpParamInts.push(entries.length); + valuesCleanups.push(() => { for (const cleanup of arrayCleanups) { cleanup(); } }); + } + instance.exports.bjs_optionalDictionary(+isSome); + const isSome1 = tmpRetInts.pop(); + let optResult; + if (isSome1) { + const dictLen = tmpRetInts.pop(); + const dictResult = {}; + for (let i = 0; i < dictLen; i++) { + const string = tmpRetStrings.pop(); + const string1 = tmpRetStrings.pop(); + dictResult[string1] = string; + } + optResult = dictResult; + } else { + optResult = null; + } + for (const cleanup of valuesCleanups) { cleanup(); } + return optResult; + }, + nestedDictionary: function bjs_nestedDictionary(values) { + const arrayCleanups = []; + const entries = Object.entries(values); + for (const entry of entries) { + const [key, value] = entry; + const bytes = textEncoder.encode(key); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); + arrayCleanups.push(() => { + swift.memory.release(id); + }); + const arrayCleanups1 = []; + for (const elem of value) { + tmpParamInts.push((elem | 0)); + } + tmpParamInts.push(value.length); + arrayCleanups.push(() => { + for (const cleanup of arrayCleanups1) { cleanup(); } + }); + } + tmpParamInts.push(entries.length); + instance.exports.bjs_nestedDictionary(); + const dictLen = tmpRetInts.pop(); + const dictResult = {}; + for (let i = 0; i < dictLen; i++) { + const arrayLen = tmpRetInts.pop(); + const arrayResult = []; + for (let i1 = 0; i1 < arrayLen; i1++) { + const int = tmpRetInts.pop(); + arrayResult.push(int); + } + arrayResult.reverse(); + const string = tmpRetStrings.pop(); + dictResult[string] = arrayResult; + } + for (const cleanup of arrayCleanups) { cleanup(); } + return dictResult; + }, + boxDictionary: function bjs_boxDictionary(boxes) { + const arrayCleanups = []; + const entries = Object.entries(boxes); + for (const entry of entries) { + const [key, value] = entry; + const bytes = textEncoder.encode(key); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); + arrayCleanups.push(() => { + swift.memory.release(id); + }); + tmpParamPointers.push(value.pointer); + } + tmpParamInts.push(entries.length); + instance.exports.bjs_boxDictionary(); + const dictLen = tmpRetInts.pop(); + const dictResult = {}; + for (let i = 0; i < dictLen; i++) { + const ptr = tmpRetPointers.pop(); + const obj = _exports['Box'].__construct(ptr); + const string = tmpRetStrings.pop(); + dictResult[string] = obj; + } + for (const cleanup of arrayCleanups) { cleanup(); } + return dictResult; + }, + optionalBoxDictionary: function bjs_optionalBoxDictionary(boxes) { + const arrayCleanups = []; + const entries = Object.entries(boxes); + for (const entry of entries) { + const [key, value] = entry; + const bytes = textEncoder.encode(key); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); + arrayCleanups.push(() => { + swift.memory.release(id); + }); + const isSome = value != null ? 1 : 0; + if (isSome) { + tmpParamPointers.push(value.pointer); + } else { + tmpParamPointers.push(0); + } + tmpParamInts.push(isSome); + } + tmpParamInts.push(entries.length); + instance.exports.bjs_optionalBoxDictionary(); + const dictLen = tmpRetInts.pop(); + const dictResult = {}; + for (let i = 0; i < dictLen; i++) { + const isSome1 = tmpRetInts.pop(); + let optValue; + if (isSome1 === 0) { + optValue = null; + } else { + const ptr = tmpRetPointers.pop(); + const obj = _exports['Box'].__construct(ptr); + optValue = obj; + } + const string = tmpRetStrings.pop(); + dictResult[string] = optValue; + } + for (const cleanup of arrayCleanups) { cleanup(); } + return dictResult; + }, + }; + _exports = exports; + return exports; + }, + } +} \ No newline at end of file diff --git a/Sources/JavaScriptKit/BridgeJSIntrinsics.swift b/Sources/JavaScriptKit/BridgeJSIntrinsics.swift index 1c859272..fd2cbd90 100644 --- a/Sources/JavaScriptKit/BridgeJSIntrinsics.swift +++ b/Sources/JavaScriptKit/BridgeJSIntrinsics.swift @@ -2375,3 +2375,122 @@ extension Array: _BridgedSwiftStackType where Element: _BridgedSwiftStackType, E bridgeJSLowerReturn() } } + +// MARK: - Dictionary Support + +public protocol _BridgedSwiftDictionaryStackType: _BridgedSwiftTypeLoweredIntoVoidType { + associatedtype DictionaryValue: _BridgedSwiftStackType + where DictionaryValue.StackLiftResult == DictionaryValue +} + +extension Dictionary: _BridgedSwiftStackType +where Key == String, Value: _BridgedSwiftStackType, Value.StackLiftResult == Value { + public typealias StackLiftResult = [String: Value] + // Lowering/return use stack-based encoding, so dictionary also behaves like a void-lowered type. + // Optional/JSUndefinedOr wrappers rely on this conformance to push an isSome flag and + // then delegate to the stack-based lowering defined below. + // swiftlint:disable:next type_name +} + +extension Dictionary: _BridgedSwiftTypeLoweredIntoVoidType, _BridgedSwiftDictionaryStackType +where Key == String, Value: _BridgedSwiftStackType, Value.StackLiftResult == Value { + public typealias DictionaryValue = Value + + @_spi(BridgeJS) public static func bridgeJSLiftParameter() -> [String: Value] { + let count = Int(_swift_js_pop_i32()) + var result: [String: Value] = [:] + result.reserveCapacity(count) + for _ in 0.. [String: Value] { + bridgeJSLiftParameter() + } + + @_spi(BridgeJS) public consuming func bridgeJSLowerStackReturn() { + bridgeJSLowerReturn() + } + + @_spi(BridgeJS) public consuming func bridgeJSLowerReturn() { + let count = Int32(self.count) + for (key, value) in self { + key.bridgeJSLowerStackReturn() + value.bridgeJSLowerStackReturn() + } + _swift_js_push_i32(count) + } + + @_spi(BridgeJS) public consuming func bridgeJSLowerParameter() { + bridgeJSLowerReturn() + } +} + +extension Optional where Wrapped: _BridgedSwiftDictionaryStackType { + typealias DictionaryValue = Wrapped.DictionaryValue + + @_spi(BridgeJS) public consuming func bridgeJSLowerParameter() -> Int32 { + switch consume self { + case .none: + return 0 + case .some(let dict): + dict.bridgeJSLowerReturn() + return 1 + } + } + + @_spi(BridgeJS) public consuming func bridgeJSLowerReturn() { + switch consume self { + case .none: + _swift_js_push_i32(0) + case .some(let dict): + dict.bridgeJSLowerReturn() + _swift_js_push_i32(1) + } + } + + @_spi(BridgeJS) public static func bridgeJSLiftParameter(_ isSome: Int32) -> [String: Wrapped.DictionaryValue]? { + if isSome == 0 { + return nil + } + return Dictionary.bridgeJSLiftParameter() + } + + @_spi(BridgeJS) public static func bridgeJSLiftParameter() -> [String: Wrapped.DictionaryValue]? { + bridgeJSLiftParameter(_swift_js_pop_i32()) + } + + @_spi(BridgeJS) public static func bridgeJSLiftReturn() -> [String: Wrapped.DictionaryValue]? { + let isSome = _swift_js_pop_i32() + if isSome == 0 { + return nil + } + return Dictionary.bridgeJSLiftParameter() + } +} + +extension _BridgedAsOptional where Wrapped: _BridgedSwiftDictionaryStackType { + typealias DictionaryValue = Wrapped.DictionaryValue + + @_spi(BridgeJS) public consuming func bridgeJSLowerParameter() -> Int32 { + let opt = optionalRepresentation + if let dict = opt { + dict.bridgeJSLowerReturn() + return 1 + } + return 0 + } + + @_spi(BridgeJS) public static func bridgeJSLiftReturn() -> Self { + let isSome = _swift_js_pop_i32() + if isSome == 0 { + return Self(optional: nil) + } + let value = Dictionary.bridgeJSLiftParameter() as! Wrapped + return Self(optional: value) + } +} diff --git a/Tests/BridgeJSRuntimeTests/DictionaryTests.swift b/Tests/BridgeJSRuntimeTests/DictionaryTests.swift new file mode 100644 index 00000000..83ca72d0 --- /dev/null +++ b/Tests/BridgeJSRuntimeTests/DictionaryTests.swift @@ -0,0 +1,96 @@ +@_spi(Experimental) @_spi(BridgeJS) import JavaScriptKit +import XCTest + +final class DictionaryTests: XCTestCase { + func testRoundTripDictionary() throws { + let input: [String: Int] = ["a": 1, "b": 2] + let result = try jsRoundTripDictionary(input) + XCTAssertEqual(result, input) + } + + func testRoundTripDictionaryBool() throws { + let input: [String: Bool] = ["yes": true, "no": false] + let result = try jsRoundTripDictionaryBool(input) + XCTAssertEqual(result, input) + } + + func testRoundTripDictionaryDouble() throws { + let input: [String: Double] = ["pi": 3.14, "tau": 6.28] + let result = try jsRoundTripDictionaryDouble(input) + XCTAssertEqual(result, input) + } + + func testRoundTripDictionaryJSObject() throws { + let global = JSObject.global + let input: [String: JSObject] = [ + "global": global + ] + let result = try jsRoundTripDictionaryJSObject(input) + XCTAssertEqual(result, input) + } + + func testRoundTripDictionaryJSValue() throws { + let input: [String: JSValue] = [ + "number": .number(123.5), + "boolean": .boolean(true), + "string": .string("hello"), + "null": .null, + ] + let result = try jsRoundTripDictionaryJSValue(input) + XCTAssertEqual(result, input) + } + + func testRoundTripNestedDictionary() throws { + let input: [String: [Double]] = [ + "xs": [1.0, 2.5], + "ys": [], + ] + let result = try jsRoundTripNestedDictionary(input) + XCTAssertEqual(result, input) + } + + func testRoundTripOptionalDictionaryNull() throws { + let some: [String: String]? = ["k": "v"] + XCTAssertEqual(try jsRoundTripOptionalDictionary(some), some) + XCTAssertNil(try jsRoundTripOptionalDictionary(nil)) + } + + func testRoundTripOptionalDictionaryUndefined() throws { + let some: JSUndefinedOr<[String: Int]> = .value(["n": 42]) + let undefined: JSUndefinedOr<[String: Int]> = .undefinedValue + + let returnedSome = try jsRoundTripUndefinedDictionary(some) + switch returnedSome { + case .value(let dict): + XCTAssertEqual(dict, ["n": 42]) + case .undefined: + XCTFail("Expected defined dictionary") + } + + let returnedUndefined = try jsRoundTripUndefinedDictionary(undefined) + switch returnedUndefined { + case .value: + XCTFail("Expected undefined") + case .undefined: + break + } + } +} + +@JSFunction func jsRoundTripDictionary(_ values: [String: Int]) throws(JSException) -> [String: Int] + +@JSFunction func jsRoundTripDictionaryBool(_ values: [String: Bool]) throws(JSException) -> [String: Bool] + +@JSFunction func jsRoundTripDictionaryDouble(_ values: [String: Double]) throws(JSException) -> [String: Double] + +@JSFunction func jsRoundTripDictionaryJSObject(_ values: [String: JSObject]) throws(JSException) -> [String: JSObject] + +@JSFunction func jsRoundTripDictionaryJSValue(_ values: [String: JSValue]) throws(JSException) -> [String: JSValue] + +@JSFunction func jsRoundTripNestedDictionary(_ values: [String: [Double]]) throws(JSException) -> [String: [Double]] + +@JSFunction func jsRoundTripOptionalDictionary(_ values: [String: String]?) throws(JSException) -> [String: String]? + +@JSFunction func jsRoundTripUndefinedDictionary( + _ values: JSUndefinedOr<[String: Int]> +) throws(JSException) -> JSUndefinedOr<[String: Int]> diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index 74817d82..7f9a2058 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -52,6 +52,14 @@ func runJsWorks() -> Void return v } +@JS func roundTripDictionaryExport(v: [String: Int]) -> [String: Int] { + return v +} + +@JS func roundTripOptionalDictionaryExport(v: [String: String]?) -> [String: String]? { + return v +} + @JS func roundTripJSValue(v: JSValue) -> JSValue { return v } diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index 194ed04b..7ca92b35 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -3810,6 +3810,28 @@ public func _bjs_roundTripJSObject(_ v: Int32) -> Int32 { #endif } +@_expose(wasm, "bjs_roundTripDictionaryExport") +@_cdecl("bjs_roundTripDictionaryExport") +public func _bjs_roundTripDictionaryExport() -> Void { + #if arch(wasm32) + let ret = roundTripDictionaryExport(v: [String: Int].bridgeJSLiftParameter()) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_roundTripOptionalDictionaryExport") +@_cdecl("bjs_roundTripOptionalDictionaryExport") +public func _bjs_roundTripOptionalDictionaryExport(_ v: Int32) -> Void { + #if arch(wasm32) + let ret = roundTripOptionalDictionaryExport(v: Optional<[String: String]>.bridgeJSLiftParameter(v)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_roundTripJSValue") @_cdecl("bjs_roundTripJSValue") public func _bjs_roundTripJSValue(_ vKind: Int32, _ vPayload1: Int32, _ vPayload2: Float64) -> Void { @@ -8437,6 +8459,150 @@ fileprivate func _bjs_Container_wrap(_ pointer: UnsafeMutableRawPointer) -> Int3 } #endif +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripDictionary") +fileprivate func bjs_jsRoundTripDictionary() -> Void +#else +fileprivate func bjs_jsRoundTripDictionary() -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsRoundTripDictionary(_ values: [String: Int]) throws(JSException) -> [String: Int] { + let _ = values.bridgeJSLowerParameter() + bjs_jsRoundTripDictionary() + if let error = _swift_js_take_exception() { + throw error + } + return [String: Int].bridgeJSLiftReturn() +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripDictionaryBool") +fileprivate func bjs_jsRoundTripDictionaryBool() -> Void +#else +fileprivate func bjs_jsRoundTripDictionaryBool() -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsRoundTripDictionaryBool(_ values: [String: Bool]) throws(JSException) -> [String: Bool] { + let _ = values.bridgeJSLowerParameter() + bjs_jsRoundTripDictionaryBool() + if let error = _swift_js_take_exception() { + throw error + } + return [String: Bool].bridgeJSLiftReturn() +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripDictionaryDouble") +fileprivate func bjs_jsRoundTripDictionaryDouble() -> Void +#else +fileprivate func bjs_jsRoundTripDictionaryDouble() -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsRoundTripDictionaryDouble(_ values: [String: Double]) throws(JSException) -> [String: Double] { + let _ = values.bridgeJSLowerParameter() + bjs_jsRoundTripDictionaryDouble() + if let error = _swift_js_take_exception() { + throw error + } + return [String: Double].bridgeJSLiftReturn() +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripDictionaryJSObject") +fileprivate func bjs_jsRoundTripDictionaryJSObject() -> Void +#else +fileprivate func bjs_jsRoundTripDictionaryJSObject() -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsRoundTripDictionaryJSObject(_ values: [String: JSObject]) throws(JSException) -> [String: JSObject] { + let _ = values.bridgeJSLowerParameter() + bjs_jsRoundTripDictionaryJSObject() + if let error = _swift_js_take_exception() { + throw error + } + return [String: JSObject].bridgeJSLiftReturn() +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripDictionaryJSValue") +fileprivate func bjs_jsRoundTripDictionaryJSValue() -> Void +#else +fileprivate func bjs_jsRoundTripDictionaryJSValue() -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsRoundTripDictionaryJSValue(_ values: [String: JSValue]) throws(JSException) -> [String: JSValue] { + let _ = values.bridgeJSLowerParameter() + bjs_jsRoundTripDictionaryJSValue() + if let error = _swift_js_take_exception() { + throw error + } + return [String: JSValue].bridgeJSLiftReturn() +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripNestedDictionary") +fileprivate func bjs_jsRoundTripNestedDictionary() -> Void +#else +fileprivate func bjs_jsRoundTripNestedDictionary() -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsRoundTripNestedDictionary(_ values: [String: [Double]]) throws(JSException) -> [String: [Double]] { + let _ = values.bridgeJSLowerParameter() + bjs_jsRoundTripNestedDictionary() + if let error = _swift_js_take_exception() { + throw error + } + return [String: [Double]].bridgeJSLiftReturn() +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripOptionalDictionary") +fileprivate func bjs_jsRoundTripOptionalDictionary(_ values: Int32) -> Void +#else +fileprivate func bjs_jsRoundTripOptionalDictionary(_ values: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsRoundTripOptionalDictionary(_ values: Optional<[String: String]>) throws(JSException) -> Optional<[String: String]> { + let valuesIsSome = values.bridgeJSLowerParameter() + bjs_jsRoundTripOptionalDictionary(valuesIsSome) + if let error = _swift_js_take_exception() { + throw error + } + return Optional<[String: String]>.bridgeJSLiftReturn() +} + +#if arch(wasm32) +@_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_jsRoundTripUndefinedDictionary") +fileprivate func bjs_jsRoundTripUndefinedDictionary(_ values: Int32) -> Void +#else +fileprivate func bjs_jsRoundTripUndefinedDictionary(_ values: Int32) -> Void { + fatalError("Only available on WebAssembly") +} +#endif + +func _$jsRoundTripUndefinedDictionary(_ values: JSUndefinedOr<[String: Int]>) throws(JSException) -> JSUndefinedOr<[String: Int]> { + let valuesIsSome = values.bridgeJSLowerParameter() + bjs_jsRoundTripUndefinedDictionary(valuesIsSome) + if let error = _swift_js_take_exception() { + throw error + } + return JSUndefinedOr<[String: Int]>.bridgeJSLiftReturn() +} + #if arch(wasm32) @_extern(wasm, module: "BridgeJSRuntimeTests", name: "bjs_Foo_init") fileprivate func bjs_Foo_init(_ value: Int32) -> Int32 diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json index 10ca73fe..8171fd95 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json @@ -5577,6 +5577,82 @@ } } }, + { + "abiName" : "bjs_roundTripDictionaryExport", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "roundTripDictionaryExport", + "parameters" : [ + { + "label" : "v", + "name" : "v", + "type" : { + "dictionary" : { + "_0" : { + "int" : { + + } + } + } + } + } + ], + "returnType" : { + "dictionary" : { + "_0" : { + "int" : { + + } + } + } + } + }, + { + "abiName" : "bjs_roundTripOptionalDictionaryExport", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "roundTripOptionalDictionaryExport", + "parameters" : [ + { + "label" : "v", + "name" : "v", + "type" : { + "nullable" : { + "_0" : { + "dictionary" : { + "_0" : { + "string" : { + + } + } + } + }, + "_1" : "null" + } + } + } + ], + "returnType" : { + "nullable" : { + "_0" : { + "dictionary" : { + "_0" : { + "string" : { + + } + } + } + }, + "_1" : "null" + } + } + }, { "abiName" : "bjs_roundTripJSValue", "effects" : { @@ -12838,6 +12914,249 @@ }, "imported" : { "children" : [ + { + "functions" : [ + { + "name" : "jsRoundTripDictionary", + "parameters" : [ + { + "name" : "values", + "type" : { + "dictionary" : { + "_0" : { + "int" : { + + } + } + } + } + } + ], + "returnType" : { + "dictionary" : { + "_0" : { + "int" : { + + } + } + } + } + }, + { + "name" : "jsRoundTripDictionaryBool", + "parameters" : [ + { + "name" : "values", + "type" : { + "dictionary" : { + "_0" : { + "bool" : { + + } + } + } + } + } + ], + "returnType" : { + "dictionary" : { + "_0" : { + "bool" : { + + } + } + } + } + }, + { + "name" : "jsRoundTripDictionaryDouble", + "parameters" : [ + { + "name" : "values", + "type" : { + "dictionary" : { + "_0" : { + "double" : { + + } + } + } + } + } + ], + "returnType" : { + "dictionary" : { + "_0" : { + "double" : { + + } + } + } + } + }, + { + "name" : "jsRoundTripDictionaryJSObject", + "parameters" : [ + { + "name" : "values", + "type" : { + "dictionary" : { + "_0" : { + "jsObject" : { + + } + } + } + } + } + ], + "returnType" : { + "dictionary" : { + "_0" : { + "jsObject" : { + + } + } + } + } + }, + { + "name" : "jsRoundTripDictionaryJSValue", + "parameters" : [ + { + "name" : "values", + "type" : { + "dictionary" : { + "_0" : { + "jsValue" : { + + } + } + } + } + } + ], + "returnType" : { + "dictionary" : { + "_0" : { + "jsValue" : { + + } + } + } + } + }, + { + "name" : "jsRoundTripNestedDictionary", + "parameters" : [ + { + "name" : "values", + "type" : { + "dictionary" : { + "_0" : { + "array" : { + "_0" : { + "double" : { + + } + } + } + } + } + } + } + ], + "returnType" : { + "dictionary" : { + "_0" : { + "array" : { + "_0" : { + "double" : { + + } + } + } + } + } + } + }, + { + "name" : "jsRoundTripOptionalDictionary", + "parameters" : [ + { + "name" : "values", + "type" : { + "nullable" : { + "_0" : { + "dictionary" : { + "_0" : { + "string" : { + + } + } + } + }, + "_1" : "null" + } + } + } + ], + "returnType" : { + "nullable" : { + "_0" : { + "dictionary" : { + "_0" : { + "string" : { + + } + } + } + }, + "_1" : "null" + } + } + }, + { + "name" : "jsRoundTripUndefinedDictionary", + "parameters" : [ + { + "name" : "values", + "type" : { + "nullable" : { + "_0" : { + "dictionary" : { + "_0" : { + "int" : { + + } + } + } + }, + "_1" : "undefined" + } + } + } + ], + "returnType" : { + "nullable" : { + "_0" : { + "dictionary" : { + "_0" : { + "int" : { + + } + } + } + }, + "_1" : "undefined" + } + } + } + ], + "types" : [ + + ] + }, { "functions" : [ diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index 92f5079d..934c4f01 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -64,6 +64,30 @@ export async function setupOptions(options, context) { "jsRoundTripString": (v) => { return v; }, + "jsRoundTripDictionary": (dict) => { + return { ...dict }; + }, + "jsRoundTripDictionaryBool": (dict) => { + return { ...dict }; + }, + "jsRoundTripDictionaryDouble": (dict) => { + return { ...dict }; + }, + "jsRoundTripDictionaryJSObject": (dict) => { + return dict; + }, + "jsRoundTripDictionaryJSValue": (dict) => { + return dict; + }, + "jsRoundTripNestedDictionary": (dict) => { + return Object.fromEntries(Object.entries(dict).map(([k, v]) => [k, [...v]])); + }, + "jsRoundTripOptionalDictionary": (dict) => { + return dict ?? null; + }, + "jsRoundTripUndefinedDictionary": (dict) => { + return dict; + }, "jsRoundTripOptionalNumberNull": (v) => { return v ?? null; }, @@ -279,6 +303,11 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { ]) { assert.equal(exports.roundTripString(v), v); } + const dict = { a: 1, b: 2 }; + assert.deepEqual(exports.roundTripDictionaryExport(dict), dict); + const optDict = { hello: "world" }; + assert.deepEqual(exports.roundTripOptionalDictionaryExport(optDict), optDict); + assert.equal(exports.roundTripOptionalDictionaryExport(null), null); const arrayStruct = { ints: [1, 2, 3], optStrings: ["a", "b"] }; const arrayStructRoundTrip = exports.roundTripArrayMembers(arrayStruct); assert.deepEqual(arrayStructRoundTrip.ints, [1, 2, 3]);