From 5c6213fb2127c11d512e8436c6336f78cde96571 Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Tue, 27 Jan 2026 21:09:33 +0700 Subject: [PATCH] BridgeJS: Fix codegen for Float/Double raw value enums in struct fields and optional context --- .../BridgeJSCore/SwiftToSkeleton.swift | 8 +- .../Sources/BridgeJSLink/JSGlueGen.swift | 176 +++++++++++++++ .../MacroSwift/EnumAssociatedValue.swift | 24 ++ .../EnumAssociatedValue.json | 207 ++++++++++++++++++ .../EnumAssociatedValue.swift | 153 +++++++++++++ .../EnumAssociatedValue.d.ts | 38 ++++ .../BridgeJSLinkTests/EnumAssociatedValue.js | 129 +++++++++++ .../Exporting-Swift/Exporting-Swift-Enum.md | 2 +- .../BridgeJSRuntimeTests/ExportAPITests.swift | 16 ++ .../Generated/BridgeJS.swift | 107 +++++++++ .../Generated/JavaScript/BridgeJS.json | 140 ++++++++++++ Tests/prelude.mjs | 38 +++- 12 files changed, 1032 insertions(+), 6 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift index d32d85f55..89322dc66 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSCore/SwiftToSkeleton.swift @@ -1428,18 +1428,18 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { for enumCase in exportedEnum.cases { for associatedValue in enumCase.associatedValues { switch associatedValue.type { - case .string, .int, .float, .double, .bool: + case .string, .int, .float, .double, .bool, .caseEnum, .rawValueEnum: break case .nullable(let wrappedType, _): switch wrappedType { - case .string, .int, .float, .double, .bool: + case .string, .int, .float, .double, .bool, .caseEnum, .rawValueEnum: break default: diagnose( node: node, message: "Unsupported associated value type: \(associatedValue.type.swiftType)", hint: - "Only primitive types and optional primitives (String?, Int?, Float?, Double?, Bool?) are supported in associated-value enums" + "Only primitive types, enums, and their optionals are supported in associated-value enums" ) } default: @@ -1447,7 +1447,7 @@ private final class ExportSwiftAPICollector: SyntaxAnyVisitor { node: node, message: "Unsupported associated value type: \(associatedValue.type.swiftType)", hint: - "Only primitive types and optional primitives (String?, Int?, Float?, Double?, Bool?) are supported in associated-value enums" + "Only primitive types, enums, and their optionals are supported in associated-value enums" ) } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift index 90ea5ab0c..be175dc35 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift @@ -1945,6 +1945,64 @@ struct IntrinsicJSFragment: Sendable { return [] } ) + case .caseEnum: + return IntrinsicJSFragment( + parameters: ["value"], + printCode: { arguments, scope, printer, cleanup in + printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push((\(arguments[0]) | 0));") + return [] + } + ) + case .rawValueEnum(_, let rawType): + switch rawType { + case .string: + return IntrinsicJSFragment( + parameters: ["value"], + printCode: { arguments, scope, printer, cleanup in + let value = arguments[0] + let bytesVar = scope.variable("bytes") + let idVar = scope.variable("id") + printer.write( + "const \(bytesVar) = \(JSGlueVariableScope.reservedTextEncoder).encode(\(value));" + ) + printer.write( + "const \(idVar) = \(JSGlueVariableScope.reservedSwift).memory.retain(\(bytesVar));" + ) + printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(\(bytesVar).length);") + printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(\(idVar));") + cleanup.write("\(JSGlueVariableScope.reservedSwift).memory.release(\(idVar));") + return [] + } + ) + case .float: + return IntrinsicJSFragment( + parameters: ["value"], + printCode: { arguments, scope, printer, cleanup in + printer.write( + "\(JSGlueVariableScope.reservedTmpParamF32s).push(Math.fround(\(arguments[0])));" + ) + return [] + } + ) + case .double: + return IntrinsicJSFragment( + parameters: ["value"], + printCode: { arguments, scope, printer, cleanup in + printer.write("\(JSGlueVariableScope.reservedTmpParamF64s).push(\(arguments[0]));") + return [] + } + ) + default: + return IntrinsicJSFragment( + parameters: ["value"], + printCode: { arguments, scope, printer, cleanup in + printer.write( + "\(JSGlueVariableScope.reservedTmpParamInts).push((\(arguments[0]) | 0));" + ) + return [] + } + ) + } case .nullable(let wrappedType, let kind): return IntrinsicJSFragment( parameters: ["value"], @@ -1999,6 +2057,68 @@ struct IntrinsicJSFragment: Sendable { "\(JSGlueVariableScope.reservedTmpParamF64s).push(\(isSomeVar) ? \(value) : 0.0);" ) printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(\(isSomeVar) ? 1 : 0);") + case .caseEnum: + printer.write( + "\(JSGlueVariableScope.reservedTmpParamInts).push(\(isSomeVar) ? (\(value) | 0) : 0);" + ) + printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(\(isSomeVar) ? 1 : 0);") + case .rawValueEnum(_, let rawType): + switch rawType { + case .string: + let idVar = scope.variable("id") + printer.write("let \(idVar);") + printer.write("if (\(isSomeVar)) {") + printer.indent { + let bytesVar = scope.variable("bytes") + printer.write( + "let \(bytesVar) = \(JSGlueVariableScope.reservedTextEncoder).encode(\(value));" + ) + printer.write( + "\(idVar) = \(JSGlueVariableScope.reservedSwift).memory.retain(\(bytesVar));" + ) + printer.write( + "\(JSGlueVariableScope.reservedTmpParamInts).push(\(bytesVar).length);" + ) + printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(\(idVar));") + } + printer.write("} else {") + printer.indent { + printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(0);") + printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(0);") + } + printer.write("}") + printer.write( + "\(JSGlueVariableScope.reservedTmpParamInts).push(\(isSomeVar) ? 1 : 0);" + ) + cleanup.write("if(\(idVar)) {") + cleanup.indent { + cleanup.write( + "\(JSGlueVariableScope.reservedSwift).memory.release(\(idVar));" + ) + } + cleanup.write("}") + case .float: + printer.write( + "\(JSGlueVariableScope.reservedTmpParamF32s).push(\(isSomeVar) ? Math.fround(\(value)) : 0.0);" + ) + printer.write( + "\(JSGlueVariableScope.reservedTmpParamInts).push(\(isSomeVar) ? 1 : 0);" + ) + case .double: + printer.write( + "\(JSGlueVariableScope.reservedTmpParamF64s).push(\(isSomeVar) ? \(value) : 0.0);" + ) + printer.write( + "\(JSGlueVariableScope.reservedTmpParamInts).push(\(isSomeVar) ? 1 : 0);" + ) + default: + printer.write( + "\(JSGlueVariableScope.reservedTmpParamInts).push(\(isSomeVar) ? (\(value) | 0) : 0);" + ) + printer.write( + "\(JSGlueVariableScope.reservedTmpParamInts).push(\(isSomeVar) ? 1 : 0);" + ) + } default: printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(\(isSomeVar) ? 1 : 0);") } @@ -2063,6 +2183,62 @@ struct IntrinsicJSFragment: Sendable { return [dVar] } ) + case .caseEnum: + return IntrinsicJSFragment( + parameters: [], + printCode: { arguments, scope, printer, cleanup in + let iVar = scope.variable("int") + printer.write("const \(iVar) = \(JSGlueVariableScope.reservedTmpRetInts).pop();") + return [iVar] + } + ) + case .rawValueEnum(_, let rawType): + switch rawType { + case .string: + return IntrinsicJSFragment( + parameters: [], + printCode: { arguments, scope, printer, cleanup in + let strVar = scope.variable("string") + printer.write( + "const \(strVar) = \(JSGlueVariableScope.reservedTmpRetStrings).pop();" + ) + return [strVar] + } + ) + case .float: + return IntrinsicJSFragment( + parameters: [], + printCode: { arguments, scope, printer, cleanup in + let fVar = scope.variable("f32") + printer.write( + "const \(fVar) = \(JSGlueVariableScope.reservedTmpRetF32s).pop();" + ) + return [fVar] + } + ) + case .double: + return IntrinsicJSFragment( + parameters: [], + printCode: { arguments, scope, printer, cleanup in + let dVar = scope.variable("f64") + printer.write( + "const \(dVar) = \(JSGlueVariableScope.reservedTmpRetF64s).pop();" + ) + return [dVar] + } + ) + default: + return IntrinsicJSFragment( + parameters: [], + printCode: { arguments, scope, printer, cleanup in + let iVar = scope.variable("int") + printer.write( + "const \(iVar) = \(JSGlueVariableScope.reservedTmpRetInts).pop();" + ) + return [iVar] + } + ) + } case .nullable(let wrappedType, let kind): return IntrinsicJSFragment( parameters: [], diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/EnumAssociatedValue.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/EnumAssociatedValue.swift index efb6cd1b1..661850e6f 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/EnumAssociatedValue.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/Inputs/MacroSwift/EnumAssociatedValue.swift @@ -55,3 +55,27 @@ enum APIOptionalResult { } @JS func roundTripOptionalAPIOptionalResult(result: APIOptionalResult?) -> APIOptionalResult? @JS func compareAPIResults(result1: APIOptionalResult?, result2: APIOptionalResult?) -> APIOptionalResult? + +@JS enum Precision: Float { + case rough = 0.1 + case fine = 0.001 +} + +@JS enum CardinalDirection { + case north + case south + case east + case west +} + +@JS +enum TypedPayloadResult { + case precision(Precision) + case direction(CardinalDirection) + case optPrecision(Precision?) + case optDirection(CardinalDirection?) + case empty +} + +@JS func roundTripTypedPayloadResult(_ result: TypedPayloadResult) -> TypedPayloadResult +@JS func roundTripOptionalTypedPayloadResult(_ result: TypedPayloadResult?) -> TypedPayloadResult? diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumAssociatedValue.json b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumAssociatedValue.json index ba9ee9324..99a63c8d6 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumAssociatedValue.json +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumAssociatedValue.json @@ -492,6 +492,153 @@ ], "swiftCallName" : "APIOptionalResult", "tsFullPath" : "APIOptionalResult" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "rough", + "rawValue" : "0.1" + }, + { + "associatedValues" : [ + + ], + "name" : "fine", + "rawValue" : "0.001" + } + ], + "emitStyle" : "const", + "name" : "Precision", + "rawType" : "Float", + "staticMethods" : [ + + ], + "staticProperties" : [ + + ], + "swiftCallName" : "Precision", + "tsFullPath" : "Precision" + }, + { + "cases" : [ + { + "associatedValues" : [ + + ], + "name" : "north" + }, + { + "associatedValues" : [ + + ], + "name" : "south" + }, + { + "associatedValues" : [ + + ], + "name" : "east" + }, + { + "associatedValues" : [ + + ], + "name" : "west" + } + ], + "emitStyle" : "const", + "name" : "CardinalDirection", + "staticMethods" : [ + + ], + "staticProperties" : [ + + ], + "swiftCallName" : "CardinalDirection", + "tsFullPath" : "CardinalDirection" + }, + { + "cases" : [ + { + "associatedValues" : [ + { + "type" : { + "rawValueEnum" : { + "_0" : "Precision", + "_1" : "Float" + } + } + } + ], + "name" : "precision" + }, + { + "associatedValues" : [ + { + "type" : { + "caseEnum" : { + "_0" : "CardinalDirection" + } + } + } + ], + "name" : "direction" + }, + { + "associatedValues" : [ + { + "type" : { + "nullable" : { + "_0" : { + "rawValueEnum" : { + "_0" : "Precision", + "_1" : "Float" + } + }, + "_1" : "null" + } + } + } + ], + "name" : "optPrecision" + }, + { + "associatedValues" : [ + { + "type" : { + "nullable" : { + "_0" : { + "caseEnum" : { + "_0" : "CardinalDirection" + } + }, + "_1" : "null" + } + } + } + ], + "name" : "optDirection" + }, + { + "associatedValues" : [ + + ], + "name" : "empty" + } + ], + "emitStyle" : "const", + "name" : "TypedPayloadResult", + "staticMethods" : [ + + ], + "staticProperties" : [ + + ], + "swiftCallName" : "TypedPayloadResult", + "tsFullPath" : "TypedPayloadResult" } ], "exposeToGlobal" : false, @@ -853,6 +1000,66 @@ "_1" : "null" } } + }, + { + "abiName" : "bjs_roundTripTypedPayloadResult", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "roundTripTypedPayloadResult", + "parameters" : [ + { + "label" : "_", + "name" : "result", + "type" : { + "associatedValueEnum" : { + "_0" : "TypedPayloadResult" + } + } + } + ], + "returnType" : { + "associatedValueEnum" : { + "_0" : "TypedPayloadResult" + } + } + }, + { + "abiName" : "bjs_roundTripOptionalTypedPayloadResult", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "roundTripOptionalTypedPayloadResult", + "parameters" : [ + { + "label" : "_", + "name" : "result", + "type" : { + "nullable" : { + "_0" : { + "associatedValueEnum" : { + "_0" : "TypedPayloadResult" + } + }, + "_1" : "null" + } + } + } + ], + "returnType" : { + "nullable" : { + "_0" : { + "associatedValueEnum" : { + "_0" : "TypedPayloadResult" + } + }, + "_1" : "null" + } + } } ], "protocols" : [ diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumAssociatedValue.swift b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumAssociatedValue.swift index 0854cba74..eee3a5602 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumAssociatedValue.swift +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSCodegenTests/EnumAssociatedValue.swift @@ -397,6 +397,137 @@ extension APIOptionalResult: _BridgedSwiftAssociatedValueEnum { } } +extension Precision: _BridgedSwiftEnumNoPayload, _BridgedSwiftRawValueEnum { +} + +extension CardinalDirection: _BridgedSwiftCaseEnum { + @_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerParameter() -> Int32 { + return bridgeJSRawValue + } + @_spi(BridgeJS) @_transparent public static func bridgeJSLiftReturn(_ value: Int32) -> CardinalDirection { + return bridgeJSLiftParameter(value) + } + @_spi(BridgeJS) @_transparent public static func bridgeJSLiftParameter(_ value: Int32) -> CardinalDirection { + return CardinalDirection(bridgeJSRawValue: value)! + } + @_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerReturn() -> Int32 { + return bridgeJSLowerParameter() + } + + private init?(bridgeJSRawValue: Int32) { + switch bridgeJSRawValue { + case 0: + self = .north + case 1: + self = .south + case 2: + self = .east + case 3: + self = .west + default: + return nil + } + } + + private var bridgeJSRawValue: Int32 { + switch self { + case .north: + return 0 + case .south: + return 1 + case .east: + return 2 + case .west: + return 3 + } + } +} + +extension TypedPayloadResult: _BridgedSwiftAssociatedValueEnum { + private static func _bridgeJSLiftFromCaseId(_ caseId: Int32) -> TypedPayloadResult { + switch caseId { + case 0: + return .precision(Precision.bridgeJSLiftParameter(_swift_js_pop_f32())) + case 1: + return .direction(CardinalDirection.bridgeJSLiftParameter(_swift_js_pop_i32())) + case 2: + return .optPrecision(Optional.bridgeJSLiftParameter()) + case 3: + return .optDirection(Optional.bridgeJSLiftParameter()) + case 4: + return .empty + default: + fatalError("Unknown TypedPayloadResult case ID: \(caseId)") + } + } + + // MARK: Protocol Export + + @_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerParameter() -> Int32 { + switch self { + case .precision(let param0): + param0.bridgeJSLowerStackReturn() + return Int32(0) + case .direction(let param0): + param0.bridgeJSLowerStackReturn() + return Int32(1) + case .optPrecision(let param0): + let __bjs_isSome_param0 = param0 != nil + if let __bjs_unwrapped_param0 = param0 { + __bjs_unwrapped_param0.bridgeJSLowerStackReturn() + } + _swift_js_push_i32(__bjs_isSome_param0 ? 1 : 0) + return Int32(2) + case .optDirection(let param0): + let __bjs_isSome_param0 = param0 != nil + if let __bjs_unwrapped_param0 = param0 { + __bjs_unwrapped_param0.bridgeJSLowerStackReturn() + } + _swift_js_push_i32(__bjs_isSome_param0 ? 1 : 0) + return Int32(3) + case .empty: + return Int32(4) + } + } + + @_spi(BridgeJS) @_transparent public static func bridgeJSLiftReturn(_ caseId: Int32) -> TypedPayloadResult { + return _bridgeJSLiftFromCaseId(caseId) + } + + // MARK: ExportSwift + + @_spi(BridgeJS) @_transparent public static func bridgeJSLiftParameter(_ caseId: Int32) -> TypedPayloadResult { + return _bridgeJSLiftFromCaseId(caseId) + } + + @_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerReturn() { + switch self { + case .precision(let param0): + _swift_js_push_tag(Int32(0)) + param0.bridgeJSLowerStackReturn() + case .direction(let param0): + _swift_js_push_tag(Int32(1)) + param0.bridgeJSLowerStackReturn() + case .optPrecision(let param0): + _swift_js_push_tag(Int32(2)) + let __bjs_isSome_param0 = param0 != nil + if let __bjs_unwrapped_param0 = param0 { + __bjs_unwrapped_param0.bridgeJSLowerStackReturn() + } + _swift_js_push_i32(__bjs_isSome_param0 ? 1 : 0) + case .optDirection(let param0): + _swift_js_push_tag(Int32(3)) + let __bjs_isSome_param0 = param0 != nil + if let __bjs_unwrapped_param0 = param0 { + __bjs_unwrapped_param0.bridgeJSLowerStackReturn() + } + _swift_js_push_i32(__bjs_isSome_param0 ? 1 : 0) + case .empty: + _swift_js_push_tag(Int32(4)) + } + } +} + @_expose(wasm, "bjs_handle") @_cdecl("bjs_handle") public func _bjs_handle(_ result: Int32) -> Void { @@ -527,4 +658,26 @@ public func _bjs_compareAPIResults(_ result1IsSome: Int32, _ result1CaseId: Int3 #else fatalError("Only available on WebAssembly") #endif +} + +@_expose(wasm, "bjs_roundTripTypedPayloadResult") +@_cdecl("bjs_roundTripTypedPayloadResult") +public func _bjs_roundTripTypedPayloadResult(_ result: Int32) -> Void { + #if arch(wasm32) + let ret = roundTripTypedPayloadResult(_: TypedPayloadResult.bridgeJSLiftParameter(result)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_roundTripOptionalTypedPayloadResult") +@_cdecl("bjs_roundTripOptionalTypedPayloadResult") +public func _bjs_roundTripOptionalTypedPayloadResult(_ resultIsSome: Int32, _ resultCaseId: Int32) -> Void { + #if arch(wasm32) + let ret = roundTripOptionalTypedPayloadResult(_: Optional.bridgeJSLiftParameter(resultIsSome, resultCaseId)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif } \ No newline at end of file diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.d.ts b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.d.ts index 20962c388..0a4540e82 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.d.ts +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.d.ts @@ -43,6 +43,33 @@ export const APIOptionalResultValues: { export type APIOptionalResultTag = { tag: typeof APIOptionalResultValues.Tag.Success; param0: string | null } | { tag: typeof APIOptionalResultValues.Tag.Failure; param0: number | null; param1: boolean | null } | { tag: typeof APIOptionalResultValues.Tag.Status; param0: boolean | null; param1: number | null; param2: string | null } +export const PrecisionValues: { + readonly Rough: 0.1; + readonly Fine: 0.001; +}; +export type PrecisionTag = typeof PrecisionValues[keyof typeof PrecisionValues]; + +export const CardinalDirectionValues: { + readonly North: 0; + readonly South: 1; + readonly East: 2; + readonly West: 3; +}; +export type CardinalDirectionTag = typeof CardinalDirectionValues[keyof typeof CardinalDirectionValues]; + +export const TypedPayloadResultValues: { + readonly Tag: { + readonly Precision: 0; + readonly Direction: 1; + readonly OptPrecision: 2; + readonly OptDirection: 3; + readonly Empty: 4; + }; +}; + +export type TypedPayloadResultTag = + { tag: typeof TypedPayloadResultValues.Tag.Precision; param0: PrecisionTag } | { tag: typeof TypedPayloadResultValues.Tag.Direction; param0: CardinalDirectionTag } | { tag: typeof TypedPayloadResultValues.Tag.OptPrecision; param0: PrecisionTag | null } | { tag: typeof TypedPayloadResultValues.Tag.OptDirection; param0: CardinalDirectionTag | null } | { tag: typeof TypedPayloadResultValues.Tag.Empty } + export type APIResultObject = typeof APIResultValues; export type ComplexResultObject = typeof ComplexResultValues; @@ -53,6 +80,12 @@ export type NetworkingResultObject = typeof API.NetworkingResultValues; export type APIOptionalResultObject = typeof APIOptionalResultValues; +export type PrecisionObject = typeof PrecisionValues; + +export type CardinalDirectionObject = typeof CardinalDirectionValues; + +export type TypedPayloadResultObject = typeof TypedPayloadResultValues; + export namespace API { const NetworkingResultValues: { readonly Tag: { @@ -87,9 +120,14 @@ export type Exports = { roundTripOptionalNetworkingResult(result: API.NetworkingResultTag | null): API.NetworkingResultTag | null; roundTripOptionalAPIOptionalResult(result: APIOptionalResultTag | null): APIOptionalResultTag | null; compareAPIResults(result1: APIOptionalResultTag | null, result2: APIOptionalResultTag | null): APIOptionalResultTag | null; + roundTripTypedPayloadResult(result: TypedPayloadResultTag): TypedPayloadResultTag; + roundTripOptionalTypedPayloadResult(result: TypedPayloadResultTag | null): TypedPayloadResultTag | null; APIResult: APIResultObject ComplexResult: ComplexResultObject APIOptionalResult: APIOptionalResultObject + Precision: PrecisionObject + CardinalDirection: CardinalDirectionObject + TypedPayloadResult: TypedPayloadResultObject API: { NetworkingResult: NetworkingResultObject }, diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.js index 6eb4b3985..54e04ef6e 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.js @@ -479,6 +479,103 @@ const __bjs_createAPIOptionalResultValuesHelpers = () => { } }); }; +export const PrecisionValues = { + Rough: 0.1, + Fine: 0.001, +}; + +export const CardinalDirectionValues = { + North: 0, + South: 1, + East: 2, + West: 3, +}; + +export const TypedPayloadResultValues = { + Tag: { + Precision: 0, + Direction: 1, + OptPrecision: 2, + OptDirection: 3, + Empty: 4, + }, +}; + +const __bjs_createTypedPayloadResultValuesHelpers = () => { + return (tmpParamInts, tmpParamF32s, tmpParamF64s, textEncoder, swift) => ({ + lower: (value) => { + const enumTag = value.tag; + switch (enumTag) { + case TypedPayloadResultValues.Tag.Precision: { + tmpParamF32s.push(Math.fround(value.param0)); + const cleanup = undefined; + return { caseId: TypedPayloadResultValues.Tag.Precision, cleanup }; + } + case TypedPayloadResultValues.Tag.Direction: { + tmpParamInts.push((value.param0 | 0)); + const cleanup = undefined; + return { caseId: TypedPayloadResultValues.Tag.Direction, cleanup }; + } + case TypedPayloadResultValues.Tag.OptPrecision: { + const isSome = value.param0 != null; + tmpParamF32s.push(isSome ? Math.fround(value.param0) : 0.0); + tmpParamInts.push(isSome ? 1 : 0); + const cleanup = undefined; + return { caseId: TypedPayloadResultValues.Tag.OptPrecision, cleanup }; + } + case TypedPayloadResultValues.Tag.OptDirection: { + const isSome = value.param0 != null; + tmpParamInts.push(isSome ? (value.param0 | 0) : 0); + tmpParamInts.push(isSome ? 1 : 0); + const cleanup = undefined; + return { caseId: TypedPayloadResultValues.Tag.OptDirection, cleanup }; + } + case TypedPayloadResultValues.Tag.Empty: { + const cleanup = undefined; + return { caseId: TypedPayloadResultValues.Tag.Empty, cleanup }; + } + default: throw new Error("Unknown TypedPayloadResultValues tag: " + String(enumTag)); + } + }, + lift: (tmpRetTag, tmpRetStrings, tmpRetInts, tmpRetF32s, tmpRetF64s) => { + const tag = tmpRetTag | 0; + switch (tag) { + case TypedPayloadResultValues.Tag.Precision: { + const f32 = tmpRetF32s.pop(); + return { tag: TypedPayloadResultValues.Tag.Precision, param0: f32 }; + } + case TypedPayloadResultValues.Tag.Direction: { + const int = tmpRetInts.pop(); + return { tag: TypedPayloadResultValues.Tag.Direction, param0: int }; + } + case TypedPayloadResultValues.Tag.OptPrecision: { + const isSome = tmpRetInts.pop(); + let optional; + if (isSome) { + const f32 = tmpRetF32s.pop(); + optional = f32; + } else { + optional = null; + } + return { tag: TypedPayloadResultValues.Tag.OptPrecision, param0: optional }; + } + case TypedPayloadResultValues.Tag.OptDirection: { + const isSome = tmpRetInts.pop(); + let optional; + if (isSome) { + const int = tmpRetInts.pop(); + optional = int; + } else { + optional = null; + } + return { tag: TypedPayloadResultValues.Tag.OptDirection, param0: optional }; + } + case TypedPayloadResultValues.Tag.Empty: return { tag: TypedPayloadResultValues.Tag.Empty }; + default: throw new Error("Unknown TypedPayloadResultValues tag returned from Swift: " + String(tag)); + } + } + }); +}; export async function createInstantiator(options, swift) { let instance; let memory; @@ -696,6 +793,9 @@ export async function createInstantiator(options, swift) { const APIOptionalResultHelpers = __bjs_createAPIOptionalResultValuesHelpers()(tmpParamInts, tmpParamF32s, tmpParamF64s, textEncoder, swift); enumHelpers.APIOptionalResult = APIOptionalResultHelpers; + const TypedPayloadResultHelpers = __bjs_createTypedPayloadResultValuesHelpers()(tmpParamInts, tmpParamF32s, tmpParamF64s, textEncoder, swift); + enumHelpers.TypedPayloadResult = TypedPayloadResultHelpers; + setException = (error) => { instance.exports._swift_js_exception.value = swift.memory.retain(error) } @@ -860,9 +960,38 @@ export async function createInstantiator(options, swift) { if (result2Cleanup) { result2Cleanup(); } return optResult; }, + roundTripTypedPayloadResult: function bjs_roundTripTypedPayloadResult(result) { + const { caseId: resultCaseId, cleanup: resultCleanup } = enumHelpers.TypedPayloadResult.lower(result); + instance.exports.bjs_roundTripTypedPayloadResult(resultCaseId); + const ret = enumHelpers.TypedPayloadResult.lift(tmpRetTag, tmpRetStrings, tmpRetInts, tmpRetF32s, tmpRetF64s); + if (resultCleanup) { resultCleanup(); } + return ret; + }, + roundTripOptionalTypedPayloadResult: function bjs_roundTripOptionalTypedPayloadResult(result) { + const isSome = result != null; + let resultCaseId, resultCleanup; + if (isSome) { + const enumResult = enumHelpers.TypedPayloadResult.lower(result); + resultCaseId = enumResult.caseId; + resultCleanup = enumResult.cleanup; + } + instance.exports.bjs_roundTripOptionalTypedPayloadResult(+isSome, isSome ? resultCaseId : 0); + const isNull = (tmpRetTag === -1); + let optResult; + if (isNull) { + optResult = null; + } else { + optResult = enumHelpers.TypedPayloadResult.lift(tmpRetTag, tmpRetStrings, tmpRetInts, tmpRetF32s, tmpRetF64s); + } + if (resultCleanup) { resultCleanup(); } + return optResult; + }, APIResult: APIResultValues, ComplexResult: ComplexResultValues, APIOptionalResult: APIOptionalResultValues, + Precision: PrecisionValues, + CardinalDirection: CardinalDirectionValues, + TypedPayloadResult: TypedPayloadResultValues, API: { NetworkingResult: NetworkingResultValues, }, diff --git a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md index 3dda2b9c5..89bf9e7e5 100644 --- a/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md +++ b/Sources/JavaScriptKit/Documentation.docc/Articles/BridgeJS/Exporting-Swift/Exporting-Swift-Enum.md @@ -510,7 +510,7 @@ This differs from classes, which use reference semantics and share state across | Static properties | ✅ | | Associated values: `String`, `Int`, `Bool`, `Float`, `Double` | ✅ | | Associated values: Custom classes/structs | ❌ | -| Associated values: Other enums | ❌ | +| Associated values: Other enums (case and raw value) | ✅ | | Associated values: Arrays/Collections | ❌ | | Associated values: Optionals | ❌ | | Generics | ❌ | diff --git a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift index 007f0b265..cb5dffd6f 100644 --- a/Tests/BridgeJSRuntimeTests/ExportAPITests.swift +++ b/Tests/BridgeJSRuntimeTests/ExportAPITests.swift @@ -578,6 +578,22 @@ typealias OptionalAge = Int? return value } +@JS enum TypedPayloadResult { + case precision(Precision) + case direction(Direction) + case optPrecision(Precision?) + case optDirection(Direction?) + case empty +} + +@JS func roundTripTypedPayloadResult(_ result: TypedPayloadResult) -> TypedPayloadResult { + return result +} + +@JS func roundTripOptionalTypedPayloadResult(_ result: TypedPayloadResult?) -> TypedPayloadResult? { + return result +} + @JS func compareAPIResults(_ r1: APIResult?, _ r2: APIResult?) -> String { let r1Str: String switch r1 { diff --git a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift index c8f428934..ddf92c6b2 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift +++ b/Tests/BridgeJSRuntimeTests/Generated/BridgeJS.swift @@ -1806,6 +1806,91 @@ extension API.NetworkingResult: _BridgedSwiftAssociatedValueEnum { } } +extension TypedPayloadResult: _BridgedSwiftAssociatedValueEnum { + private static func _bridgeJSLiftFromCaseId(_ caseId: Int32) -> TypedPayloadResult { + switch caseId { + case 0: + return .precision(Precision.bridgeJSLiftParameter(_swift_js_pop_f32())) + case 1: + return .direction(Direction.bridgeJSLiftParameter(_swift_js_pop_i32())) + case 2: + return .optPrecision(Optional.bridgeJSLiftParameter()) + case 3: + return .optDirection(Optional.bridgeJSLiftParameter()) + case 4: + return .empty + default: + fatalError("Unknown TypedPayloadResult case ID: \(caseId)") + } + } + + // MARK: Protocol Export + + @_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerParameter() -> Int32 { + switch self { + case .precision(let param0): + param0.bridgeJSLowerStackReturn() + return Int32(0) + case .direction(let param0): + param0.bridgeJSLowerStackReturn() + return Int32(1) + case .optPrecision(let param0): + let __bjs_isSome_param0 = param0 != nil + if let __bjs_unwrapped_param0 = param0 { + __bjs_unwrapped_param0.bridgeJSLowerStackReturn() + } + _swift_js_push_i32(__bjs_isSome_param0 ? 1 : 0) + return Int32(2) + case .optDirection(let param0): + let __bjs_isSome_param0 = param0 != nil + if let __bjs_unwrapped_param0 = param0 { + __bjs_unwrapped_param0.bridgeJSLowerStackReturn() + } + _swift_js_push_i32(__bjs_isSome_param0 ? 1 : 0) + return Int32(3) + case .empty: + return Int32(4) + } + } + + @_spi(BridgeJS) @_transparent public static func bridgeJSLiftReturn(_ caseId: Int32) -> TypedPayloadResult { + return _bridgeJSLiftFromCaseId(caseId) + } + + // MARK: ExportSwift + + @_spi(BridgeJS) @_transparent public static func bridgeJSLiftParameter(_ caseId: Int32) -> TypedPayloadResult { + return _bridgeJSLiftFromCaseId(caseId) + } + + @_spi(BridgeJS) @_transparent public consuming func bridgeJSLowerReturn() { + switch self { + case .precision(let param0): + _swift_js_push_tag(Int32(0)) + param0.bridgeJSLowerStackReturn() + case .direction(let param0): + _swift_js_push_tag(Int32(1)) + param0.bridgeJSLowerStackReturn() + case .optPrecision(let param0): + _swift_js_push_tag(Int32(2)) + let __bjs_isSome_param0 = param0 != nil + if let __bjs_unwrapped_param0 = param0 { + __bjs_unwrapped_param0.bridgeJSLowerStackReturn() + } + _swift_js_push_i32(__bjs_isSome_param0 ? 1 : 0) + case .optDirection(let param0): + _swift_js_push_tag(Int32(3)) + let __bjs_isSome_param0 = param0 != nil + if let __bjs_unwrapped_param0 = param0 { + __bjs_unwrapped_param0.bridgeJSLowerStackReturn() + } + _swift_js_push_i32(__bjs_isSome_param0 ? 1 : 0) + case .empty: + _swift_js_push_tag(Int32(4)) + } + } +} + extension APIOptionalResult: _BridgedSwiftAssociatedValueEnum { private static func _bridgeJSLiftFromCaseId(_ caseId: Int32) -> APIOptionalResult { switch caseId { @@ -4429,6 +4514,28 @@ public func _bjs_roundTripOptionalAPIResult(_ valueIsSome: Int32, _ valueCaseId: #endif } +@_expose(wasm, "bjs_roundTripTypedPayloadResult") +@_cdecl("bjs_roundTripTypedPayloadResult") +public func _bjs_roundTripTypedPayloadResult(_ result: Int32) -> Void { + #if arch(wasm32) + let ret = roundTripTypedPayloadResult(_: TypedPayloadResult.bridgeJSLiftParameter(result)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + +@_expose(wasm, "bjs_roundTripOptionalTypedPayloadResult") +@_cdecl("bjs_roundTripOptionalTypedPayloadResult") +public func _bjs_roundTripOptionalTypedPayloadResult(_ resultIsSome: Int32, _ resultCaseId: Int32) -> Void { + #if arch(wasm32) + let ret = roundTripOptionalTypedPayloadResult(_: Optional.bridgeJSLiftParameter(resultIsSome, resultCaseId)) + return ret.bridgeJSLowerReturn() + #else + fatalError("Only available on WebAssembly") + #endif +} + @_expose(wasm, "bjs_compareAPIResults") @_cdecl("bjs_compareAPIResults") public func _bjs_compareAPIResults(_ r1IsSome: Int32, _ r1CaseId: Int32, _ r2IsSome: Int32, _ r2CaseId: Int32) -> Void { diff --git a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json index 0cb32079a..1320b1b68 100644 --- a/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json +++ b/Tests/BridgeJSRuntimeTests/Generated/JavaScript/BridgeJS.json @@ -4357,6 +4357,86 @@ "swiftCallName" : "API.NetworkingResult", "tsFullPath" : "API.NetworkingResult" }, + { + "cases" : [ + { + "associatedValues" : [ + { + "type" : { + "rawValueEnum" : { + "_0" : "Precision", + "_1" : "Float" + } + } + } + ], + "name" : "precision" + }, + { + "associatedValues" : [ + { + "type" : { + "caseEnum" : { + "_0" : "Direction" + } + } + } + ], + "name" : "direction" + }, + { + "associatedValues" : [ + { + "type" : { + "nullable" : { + "_0" : { + "rawValueEnum" : { + "_0" : "Precision", + "_1" : "Float" + } + }, + "_1" : "null" + } + } + } + ], + "name" : "optPrecision" + }, + { + "associatedValues" : [ + { + "type" : { + "nullable" : { + "_0" : { + "caseEnum" : { + "_0" : "Direction" + } + }, + "_1" : "null" + } + } + } + ], + "name" : "optDirection" + }, + { + "associatedValues" : [ + + ], + "name" : "empty" + } + ], + "emitStyle" : "const", + "name" : "TypedPayloadResult", + "staticMethods" : [ + + ], + "staticProperties" : [ + + ], + "swiftCallName" : "TypedPayloadResult", + "tsFullPath" : "TypedPayloadResult" + }, { "cases" : [ { @@ -7497,6 +7577,66 @@ } } }, + { + "abiName" : "bjs_roundTripTypedPayloadResult", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "roundTripTypedPayloadResult", + "parameters" : [ + { + "label" : "_", + "name" : "result", + "type" : { + "associatedValueEnum" : { + "_0" : "TypedPayloadResult" + } + } + } + ], + "returnType" : { + "associatedValueEnum" : { + "_0" : "TypedPayloadResult" + } + } + }, + { + "abiName" : "bjs_roundTripOptionalTypedPayloadResult", + "effects" : { + "isAsync" : false, + "isStatic" : false, + "isThrows" : false + }, + "name" : "roundTripOptionalTypedPayloadResult", + "parameters" : [ + { + "label" : "_", + "name" : "result", + "type" : { + "nullable" : { + "_0" : { + "associatedValueEnum" : { + "_0" : "TypedPayloadResult" + } + }, + "_1" : "null" + } + } + } + ], + "returnType" : { + "nullable" : { + "_0" : { + "associatedValueEnum" : { + "_0" : "TypedPayloadResult" + } + }, + "_1" : "null" + } + } + }, { "abiName" : "bjs_compareAPIResults", "effects" : { diff --git a/Tests/prelude.mjs b/Tests/prelude.mjs index dc904579d..2585abf36 100644 --- a/Tests/prelude.mjs +++ b/Tests/prelude.mjs @@ -1,7 +1,7 @@ // @ts-check import { - DirectionValues, StatusValues, ThemeValues, HttpStatusValues, TSDirection, TSTheme, APIResultValues, ComplexResultValues, APIOptionalResultValues, StaticCalculatorValues, StaticPropertyEnumValues + DirectionValues, StatusValues, ThemeValues, HttpStatusValues, TSDirection, TSTheme, APIResultValues, ComplexResultValues, APIOptionalResultValues, StaticCalculatorValues, StaticPropertyEnumValues, PrecisionValues, TypedPayloadResultValues } from '../.build/plugins/PackageToJS/outputs/PackageTests/bridge-js.js'; /** @type {import('../.build/plugins/PackageToJS/outputs/PackageTests/test.d.ts').SetupOptionsFn} */ @@ -761,6 +761,42 @@ function BridgeJSRuntimeTests_runJsWorks(instance, exports) { assert.deepEqual(exports.roundTripOptionalAPIOptionalResult(aor10), aor10); assert.equal(exports.roundTripOptionalAPIOptionalResult(null), null); + // TypedPayloadResult — rawValueEnum and caseEnum as associated value payloads + assert.equal(exports.Precision.Rough, 0.1); + assert.equal(exports.Precision.Fine, 0.001); + + const tpr_precision = { tag: exports.TypedPayloadResult.Tag.Precision, param0: Math.fround(0.1) }; + assert.deepEqual(exports.roundTripTypedPayloadResult(tpr_precision), tpr_precision); + + const tpr_direction = { tag: exports.TypedPayloadResult.Tag.Direction, param0: exports.Direction.North }; + assert.deepEqual(exports.roundTripTypedPayloadResult(tpr_direction), tpr_direction); + + const tpr_dirSouth = { tag: exports.TypedPayloadResult.Tag.Direction, param0: exports.Direction.South }; + assert.deepEqual(exports.roundTripTypedPayloadResult(tpr_dirSouth), tpr_dirSouth); + + const tpr_optPrecisionSome = { tag: exports.TypedPayloadResult.Tag.OptPrecision, param0: Math.fround(0.001) }; + assert.deepEqual(exports.roundTripTypedPayloadResult(tpr_optPrecisionSome), tpr_optPrecisionSome); + + const tpr_optPrecisionNull = { tag: exports.TypedPayloadResult.Tag.OptPrecision, param0: null }; + assert.deepEqual(exports.roundTripTypedPayloadResult(tpr_optPrecisionNull), tpr_optPrecisionNull); + + const tpr_optDirectionSome = { tag: exports.TypedPayloadResult.Tag.OptDirection, param0: exports.Direction.East }; + assert.deepEqual(exports.roundTripTypedPayloadResult(tpr_optDirectionSome), tpr_optDirectionSome); + + const tpr_optDirectionNull = { tag: exports.TypedPayloadResult.Tag.OptDirection, param0: null }; + assert.deepEqual(exports.roundTripTypedPayloadResult(tpr_optDirectionNull), tpr_optDirectionNull); + + const tpr_empty = { tag: exports.TypedPayloadResult.Tag.Empty }; + assert.deepEqual(exports.roundTripTypedPayloadResult(tpr_empty), tpr_empty); + + // Optional TypedPayloadResult roundtrip + assert.deepEqual(exports.roundTripOptionalTypedPayloadResult(tpr_precision), tpr_precision); + assert.deepEqual(exports.roundTripOptionalTypedPayloadResult(tpr_direction), tpr_direction); + assert.deepEqual(exports.roundTripOptionalTypedPayloadResult(tpr_optPrecisionSome), tpr_optPrecisionSome); + assert.deepEqual(exports.roundTripOptionalTypedPayloadResult(tpr_optPrecisionNull), tpr_optPrecisionNull); + assert.deepEqual(exports.roundTripOptionalTypedPayloadResult(tpr_empty), tpr_empty); + assert.equal(exports.roundTripOptionalTypedPayloadResult(null), null); + assert.equal(exports.MathUtils.add(2147483647, 0), 2147483647); assert.equal(exports.StaticCalculator.roundtrip(42), 42); assert.equal(StaticCalculatorValues.Scientific, 0);