From 13e34bc9f7745a0638503d5f941f9122ef3d0f9a Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Mon, 1 Sep 2025 11:21:06 +0200 Subject: [PATCH 1/4] BrigdeJS: Migrate to IntrinsicJSFragment for individual cases --- .../Sources/BridgeJSLink/BridgeJSLink.swift | 329 ++++++++---------- .../Sources/BridgeJSLink/JSGlueGen.swift | 123 +++++++ .../EnumAssociatedValue.Export.js | 214 ++++++------ 3 files changed, 376 insertions(+), 290 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 494351db..59088974 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -666,208 +666,171 @@ struct BridgeJSLink { } } case .associatedValue: - do { - jsLines.append("const \(enumDefinition.name) = {") - jsLines.append("Tag: {".indent(count: 4)) - for (caseIndex, enumCase) in enumDefinition.cases.enumerated() { - let caseName = enumCase.name.capitalizedFirstLetter - jsLines.append("\(caseName): \(caseIndex),".indent(count: 8)) - } - jsLines.append("}".indent(count: 4)) - jsLines.append("};") - jsLines.append("") - jsLines.append("const __bjs_create\(enumDefinition.name)Helpers = () => {") - jsLines.append( - "return (\(JSGlueVariableScope.reservedTmpParamInts), \(JSGlueVariableScope.reservedTmpParamF32s), \(JSGlueVariableScope.reservedTmpParamF64s), textEncoder, swift) => ({" - .indent(count: 4) - ) + // Use the new IntrinsicJSFragment for associated value payload handling + jsLines.append("const \(enumDefinition.name) = {") + jsLines.append("Tag: {".indent(count: 4)) + for (index, enumCase) in enumDefinition.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + jsLines.append("\(caseName): \(index),".indent(count: 8)) + } + jsLines.append("}".indent(count: 4)) + jsLines.append("};") + jsLines.append("") + jsLines.append("const __bjs_create\(enumDefinition.name)Helpers = () => {") + jsLines.append( + "return (\(JSGlueVariableScope.reservedTmpParamInts), \(JSGlueVariableScope.reservedTmpParamF32s), \(JSGlueVariableScope.reservedTmpParamF64s), textEncoder, \(JSGlueVariableScope.reservedSwift)) => ({" + .indent( + count: 4 + ) + ) - jsLines.append("lower: (value) => {".indent(count: 8)) - jsLines.append("const enumTag = value.tag;".indent(count: 12)) - jsLines.append("switch (enumTag) {".indent(count: 12)) - enumDefinition.cases.forEach { enumCase in - let caseName = enumCase.name.capitalizedFirstLetter - if enumCase.associatedValues.isEmpty { - jsLines.append("case \(enumDefinition.name).Tag.\(caseName): {".indent(count: 16)) + jsLines.append("lower: (value) => {".indent(count: 8)) + jsLines.append("const enumTag = value.tag;".indent(count: 12)) + jsLines.append("switch (enumTag) {".indent(count: 12)) + enumDefinition.cases.forEach { enumCase in + let caseName = enumCase.name.capitalizedFirstLetter + if enumCase.associatedValues.isEmpty { + jsLines.append("case \(enumDefinition.name).Tag.\(caseName): {".indent(count: 16)) + jsLines.append("const cleanup = undefined;".indent(count: 20)) + jsLines.append( + "return { caseId: \(enumDefinition.name).Tag.\(caseName), cleanup };" + .indent(count: 20) + ) + jsLines.append("}".indent(count: 16)) + } else { + let scope = JSGlueVariableScope() + let cleanup = CodeFragmentPrinter() + cleanup.indent() + cleanup.indent() + cleanup.indent() + cleanup.indent() + cleanup.indent() + cleanup.indent() + let printer = CodeFragmentPrinter() + + let reversedValues = enumCase.associatedValues.enumerated().reversed() + + for (associatedValueIndex, associatedValue) in reversedValues { + let prop = associatedValue.label ?? "param\(associatedValueIndex)" + let fragment = IntrinsicJSFragment.associatedValuePushPayload(type: associatedValue.type) + + _ = fragment.printCode(["value.\(prop)"], scope, printer, cleanup) + } + + jsLines.append("case \(enumDefinition.name).Tag.\(caseName): {".indent(count: 16)) + jsLines.append(contentsOf: printer.lines.map { $0.indent(count: 20) }) + if cleanup.lines.isEmpty { jsLines.append("const cleanup = undefined;".indent(count: 20)) - jsLines.append( - "return { caseId: \(enumDefinition.name).Tag.\(caseName), cleanup };" - .indent(count: 20) - ) - jsLines.append("}".indent(count: 16)) } else { - jsLines.append("case \(enumDefinition.name).Tag.\(caseName): {".indent(count: 16)) - var pushLines: [String] = [] - var releaseIds: [String] = [] - let reversedValues = enumCase.associatedValues.enumerated().reversed() - for (associatedValueIndex, associatedValue) in reversedValues { - let prop = associatedValue.label ?? "param\(associatedValueIndex)" - switch associatedValue.type { - case .string: - let bytesVar = "bytes_\(associatedValueIndex)" - let idVar = "bytesId_\(associatedValueIndex)" - pushLines.append( - "const \(bytesVar) = \(JSGlueVariableScope.reservedTextEncoder).encode(value.\(prop));" - ) - pushLines.append( - "const \(idVar) = \(JSGlueVariableScope.reservedSwift).memory.retain(\(bytesVar));" - ) - pushLines.append( - "\(JSGlueVariableScope.reservedTmpParamInts).push(\(bytesVar).length);" - ) - pushLines.append("\(JSGlueVariableScope.reservedTmpParamInts).push(\(idVar));") - releaseIds.append(idVar) - case .bool: - pushLines.append( - "\(JSGlueVariableScope.reservedTmpParamInts).push(value.\(prop) ? 1 : 0);" - ) - case .int: - pushLines.append( - "\(JSGlueVariableScope.reservedTmpParamInts).push((value.\(prop) | 0));" - ) - case .float: - pushLines.append( - "\(JSGlueVariableScope.reservedTmpParamF32s).push(Math.fround(value.\(prop)));" - ) - case .double: - pushLines.append("\(JSGlueVariableScope.reservedTmpParamF64s).push(value.\(prop));") - default: - pushLines.append("\(JSGlueVariableScope.reservedTmpParamInts).push(0);") - } - } - jsLines.append(contentsOf: pushLines.map { $0.indent(count: 20) }) - if releaseIds.isEmpty { - jsLines.append("const cleanup = undefined;".indent(count: 20)) - } else { - jsLines.append("const cleanup = () => {".indent(count: 20)) - for id in releaseIds { - jsLines.append( - "\(JSGlueVariableScope.reservedSwift).memory.release(\(id));".indent(count: 24) - ) - } - jsLines.append("};".indent(count: 20)) - } - jsLines.append( - "return { caseId: \(enumDefinition.name).Tag.\(caseName), cleanup };" - .indent(count: 20) - ) - jsLines.append("}".indent(count: 16)) + jsLines.append("const cleanup = () => {".indent(count: 20)) + jsLines.append(contentsOf: cleanup.lines) + jsLines.append("};".indent(count: 20)) } + jsLines.append( + "return { caseId: \(enumDefinition.name).Tag.\(caseName), cleanup };" + .indent(count: 20) + ) + jsLines.append("}".indent(count: 16)) } - jsLines.append( - "default: throw new Error(\"Unknown \(enumDefinition.name) tag: \" + String(enumTag));".indent( + } + jsLines.append( + "default: throw new Error(\"Unknown \(enumDefinition.name) tag: \" + String(enumTag));".indent( + count: 16 + ) + ) + jsLines.append("}".indent(count: 12)) + jsLines.append("},".indent(count: 8)) + + jsLines.append( + "raise: (\(JSGlueVariableScope.reservedTmpRetTag), \(JSGlueVariableScope.reservedTmpRetStrings), \(JSGlueVariableScope.reservedTmpRetInts), \(JSGlueVariableScope.reservedTmpRetF32s), \(JSGlueVariableScope.reservedTmpRetF64s)) => {" + .indent( + count: 8 + ) + ) + jsLines.append("const tag = tmpRetTag | 0;".indent(count: 12)) + jsLines.append("switch (tag) {".indent(count: 12)) + enumDefinition.cases.forEach { enumCase in + let caseName = enumCase.name.capitalizedFirstLetter + if enumCase.associatedValues.isEmpty { + jsLines.append( + "case \(enumDefinition.name).Tag.\(caseName): return { tag: \(enumDefinition.name).Tag.\(caseName) };" + .indent(count: 16) + ) + } else { + var fieldPairs: [String] = [] + let scope = JSGlueVariableScope() + let printer = CodeFragmentPrinter() + let cleanup = CodeFragmentPrinter() + // Use the new IntrinsicJSFragment for associated value payload handling + for (associatedValueIndex, associatedValue) in enumCase.associatedValues.enumerated().reversed() { + let prop = associatedValue.label ?? "param\(associatedValueIndex)" + let fragment = IntrinsicJSFragment.associatedValuePopPayload(type: associatedValue.type) + + let result = fragment.printCode([], scope, printer, cleanup) + let varName = result.first ?? "value_\(associatedValueIndex)" + + fieldPairs.append("\(prop): \(varName)") + } + + jsLines.append("case \(enumDefinition.name).Tag.\(caseName): {".indent(count: 16)) + jsLines.append(contentsOf: printer.lines.map { $0.indent(count: 20) }) + jsLines.append( + "return { tag: \(enumDefinition.name).Tag.\(caseName), \(fieldPairs.reversed().joined(separator: ", ")) };" + .indent(count: 20) + ) + jsLines.append("}".indent(count: 16)) + } + } + jsLines.append( + "default: throw new Error(\"Unknown \(enumDefinition.name) tag returned from Swift: \" + String(tag));" + .indent( count: 16 ) - ) - jsLines.append("}".indent(count: 12)) - jsLines.append("},".indent(count: 8)) + ) + jsLines.append("}".indent(count: 12)) + jsLines.append("}".indent(count: 8)) + jsLines.append("});".indent(count: 4)) + jsLines.append("};") - jsLines.append( - "raise: (\(JSGlueVariableScope.reservedTmpRetTag), \(JSGlueVariableScope.reservedTmpRetStrings), \(JSGlueVariableScope.reservedTmpRetInts), \(JSGlueVariableScope.reservedTmpRetF32s), \(JSGlueVariableScope.reservedTmpRetF64s)) => {" - .indent( - count: 8 - ) - ) - jsLines.append("const tag = tmpRetTag | 0;".indent(count: 12)) - jsLines.append("switch (tag) {".indent(count: 12)) - enumDefinition.cases.forEach { enumCase in + if enumDefinition.namespace == nil { + dtsLines.append("export const \(enumDefinition.name): {") + dtsLines.append("readonly Tag: {".indent(count: 4)) + for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter + dtsLines.append("readonly \(caseName): \(index);".indent(count: 8)) + } + dtsLines.append("};".indent(count: 4)) + dtsLines.append("};") + dtsLines.append("") + var unionParts: [String] = [] + for enumCase in enumDefinition.cases { if enumCase.associatedValues.isEmpty { - jsLines.append( - "case \(enumDefinition.name).Tag.\(caseName): return { tag: \(enumDefinition.name).Tag.\(caseName) };" - .indent(count: 16) + unionParts.append( + "{ tag: typeof \(enumDefinition.name).Tag.\(enumCase.name.capitalizedFirstLetter) }" ) } else { - var locals: [String] = [] - var fieldPairs: [String] = [] - for (associatedValueIndex, associatedValue) in enumCase.associatedValues.enumerated().reversed() + var fields: [String] = [ + "tag: typeof \(enumDefinition.name).Tag.\(enumCase.name.capitalizedFirstLetter)" + ] + for (associatedValueIndex, associatedValue) in enumCase.associatedValues + .enumerated() { let prop = associatedValue.label ?? "param\(associatedValueIndex)" + let ts: String switch associatedValue.type { - case .string: - let strVar = "string_\(associatedValueIndex)" - locals.append("const \(strVar) = tmpRetStrings.pop();") - fieldPairs.append("\(prop): \(strVar)") - case .bool: - let bVar = "bool_\(associatedValueIndex)" - locals.append("const \(bVar) = tmpRetInts.pop();") - fieldPairs.append("\(prop): \(bVar)") - case .int: - let iVar = "int_\(associatedValueIndex)" - locals.append("const \(iVar) = tmpRetInts.pop();") - fieldPairs.append("\(prop): \(iVar)") - case .float: - let fVar = "f32_\(associatedValueIndex)" - locals.append("const \(fVar) = tmpRetF32s.pop();") - fieldPairs.append("\(prop): \(fVar)") - case .double: - let dVar = "f64_\(associatedValueIndex)" - locals.append("const \(dVar) = tmpRetF64s.pop();") - fieldPairs.append("\(prop): \(dVar)") - default: - fieldPairs.append("\(prop): undefined") - } - } - jsLines.append("case \(enumDefinition.name).Tag.\(caseName): {".indent(count: 16)) - jsLines.append(contentsOf: locals.map { $0.indent(count: 20) }) - jsLines.append( - "return { tag: \(enumDefinition.name).Tag.\(caseName), \(fieldPairs.reversed().joined(separator: ", ")) };" - .indent(count: 20) - ) - jsLines.append("}".indent(count: 16)) - } - } - jsLines.append( - "default: throw new Error(\"Unknown \(enumDefinition.name) tag returned from Swift: \" + String(tag));" - .indent( - count: 16 - ) - ) - jsLines.append("}".indent(count: 12)) - jsLines.append("}".indent(count: 8)) - jsLines.append("});".indent(count: 4)) - jsLines.append("};") - - if enumDefinition.namespace == nil { - dtsLines.append("export const \(enumDefinition.name): {") - dtsLines.append("readonly Tag: {".indent(count: 4)) - for (index, enumCase) in enumDefinition.cases.enumerated() { - let caseName = enumCase.name.capitalizedFirstLetter - dtsLines.append("readonly \(caseName): \(index);".indent(count: 8)) - } - dtsLines.append("};".indent(count: 4)) - dtsLines.append("};") - dtsLines.append("") - var unionParts: [String] = [] - for enumCase in enumDefinition.cases { - if enumCase.associatedValues.isEmpty { - unionParts.append( - "{ tag: typeof \(enumDefinition.name).Tag.\(enumCase.name.capitalizedFirstLetter) }" - ) - } else { - var fields: [String] = [ - "tag: typeof \(enumDefinition.name).Tag.\(enumCase.name.capitalizedFirstLetter)" - ] - for (associatedValueIndex, associatedValue) in enumCase.associatedValues - .enumerated() - { - let prop = associatedValue.label ?? "param\(associatedValueIndex)" - let ts: String - switch associatedValue.type { - case .string: ts = "string" - case .bool: ts = "boolean" - case .int, .float, .double: ts = "number" - default: ts = "never" - } - fields.append("\(prop): \(ts)") + case .string: ts = "string" + case .bool: ts = "boolean" + case .int, .float, .double: ts = "number" + default: ts = "never" } - unionParts.append("{ \(fields.joined(separator: "; ")) }") + fields.append("\(prop): \(ts)") } + unionParts.append("{ \(fields.joined(separator: "; ")) }") } - dtsLines.append("export type \(enumDefinition.name) =") - dtsLines.append(" " + unionParts.joined(separator: " | ")) - dtsLines.append("") } + dtsLines.append("export type \(enumDefinition.name) =") + dtsLines.append(" " + unionParts.joined(separator: " | ")) + dtsLines.append("") } case .namespace: break diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift index f559e62f..3875f0f0 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift @@ -239,6 +239,129 @@ struct IntrinsicJSFragment: Sendable { ) } + // MARK: - Associated Value Payload Fragments + + /// Fragment for pushing associated value payloads during enum lowering + static func associatedValuePushPayload(type: BridgeType) -> IntrinsicJSFragment { + switch type { + 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 .bool: + return IntrinsicJSFragment( + parameters: ["value"], + printCode: { arguments, scope, printer, cleanup in + printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(\(arguments[0]) ? 1 : 0);") + return [] + } + ) + case .int: + return IntrinsicJSFragment( + parameters: ["value"], + printCode: { arguments, scope, printer, cleanup in + printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push((\(arguments[0]) | 0));") + 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: + // For unsupported types, push a default value + return IntrinsicJSFragment( + parameters: ["value"], + printCode: { arguments, scope, printer, cleanup in + printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(0);") + return [] + } + ) + } + } + + /// Fragment for popping associated value payloads during enum lifting + static func associatedValuePopPayload(type: BridgeType) -> IntrinsicJSFragment { + switch type { + 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 .bool: + return IntrinsicJSFragment( + parameters: [], + printCode: { arguments, scope, printer, cleanup in + let bVar = scope.variable("bool") + printer.write("const \(bVar) = \(JSGlueVariableScope.reservedTmpRetInts).pop();") + return [bVar] + } + ) + case .int: + return IntrinsicJSFragment( + parameters: [], + printCode: { arguments, scope, printer, cleanup in + let iVar = scope.variable("int") + printer.write("const \(iVar) = \(JSGlueVariableScope.reservedTmpRetInts).pop();") + return [iVar] + } + ) + 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: + // For unsupported types, return undefined + return IntrinsicJSFragment( + parameters: [], + printCode: { arguments, scope, printer, cleanup in + return ["undefined"] + } + ) + } + } + // MARK: - ExportSwift /// Returns a fragment that lowers a JS value to Wasm core values for parameters diff --git a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.Export.js b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.Export.js index 7a189d19..3b8e1f83 100644 --- a/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.Export.js +++ b/Plugins/BridgeJS/Tests/BridgeJSToolTests/__Snapshots__/BridgeJSLinkTests/EnumAssociatedValue.Export.js @@ -21,12 +21,12 @@ const __bjs_createAPIResultHelpers = () => { const enumTag = value.tag; switch (enumTag) { case APIResult.Tag.Success: { - const bytes_0 = textEncoder.encode(value.param0); - const bytesId_0 = swift.memory.retain(bytes_0); - tmpParamInts.push(bytes_0.length); - tmpParamInts.push(bytesId_0); + const bytes = textEncoder.encode(value.param0); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); const cleanup = () => { - swift.memory.release(bytesId_0); + swift.memory.release(id); }; return { caseId: APIResult.Tag.Success, cleanup }; } @@ -61,24 +61,24 @@ const __bjs_createAPIResultHelpers = () => { const tag = tmpRetTag | 0; switch (tag) { case APIResult.Tag.Success: { - const string_0 = tmpRetStrings.pop(); - return { tag: APIResult.Tag.Success, param0: string_0 }; + const string = tmpRetStrings.pop(); + return { tag: APIResult.Tag.Success, param0: string }; } case APIResult.Tag.Failure: { - const int_0 = tmpRetInts.pop(); - return { tag: APIResult.Tag.Failure, param0: int_0 }; + const int = tmpRetInts.pop(); + return { tag: APIResult.Tag.Failure, param0: int }; } case APIResult.Tag.Flag: { - const bool_0 = tmpRetInts.pop(); - return { tag: APIResult.Tag.Flag, param0: bool_0 }; + const bool = tmpRetInts.pop(); + return { tag: APIResult.Tag.Flag, param0: bool }; } case APIResult.Tag.Rate: { - const f32_0 = tmpRetF32s.pop(); - return { tag: APIResult.Tag.Rate, param0: f32_0 }; + const f32 = tmpRetF32s.pop(); + return { tag: APIResult.Tag.Rate, param0: f32 }; } case APIResult.Tag.Precise: { - const f64_0 = tmpRetF64s.pop(); - return { tag: APIResult.Tag.Precise, param0: f64_0 }; + const f64 = tmpRetF64s.pop(); + return { tag: APIResult.Tag.Precise, param0: f64 }; } case APIResult.Tag.Info: return { tag: APIResult.Tag.Info }; default: throw new Error("Unknown APIResult tag returned from Swift: " + String(tag)); @@ -103,35 +103,35 @@ const __bjs_createComplexResultHelpers = () => { const enumTag = value.tag; switch (enumTag) { case ComplexResult.Tag.Success: { - const bytes_0 = textEncoder.encode(value.param0); - const bytesId_0 = swift.memory.retain(bytes_0); - tmpParamInts.push(bytes_0.length); - tmpParamInts.push(bytesId_0); + const bytes = textEncoder.encode(value.param0); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); const cleanup = () => { - swift.memory.release(bytesId_0); + swift.memory.release(id); }; return { caseId: ComplexResult.Tag.Success, cleanup }; } case ComplexResult.Tag.Error: { tmpParamInts.push((value.param1 | 0)); - const bytes_0 = textEncoder.encode(value.param0); - const bytesId_0 = swift.memory.retain(bytes_0); - tmpParamInts.push(bytes_0.length); - tmpParamInts.push(bytesId_0); + const bytes = textEncoder.encode(value.param0); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); const cleanup = () => { - swift.memory.release(bytesId_0); + swift.memory.release(id); }; return { caseId: ComplexResult.Tag.Error, cleanup }; } case ComplexResult.Tag.Status: { - const bytes_2 = textEncoder.encode(value.param2); - const bytesId_2 = swift.memory.retain(bytes_2); - tmpParamInts.push(bytes_2.length); - tmpParamInts.push(bytesId_2); + const bytes = textEncoder.encode(value.param2); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); tmpParamInts.push((value.param1 | 0)); tmpParamInts.push(value.param0 ? 1 : 0); const cleanup = () => { - swift.memory.release(bytesId_2); + swift.memory.release(id); }; return { caseId: ComplexResult.Tag.Status, cleanup }; } @@ -143,18 +143,18 @@ const __bjs_createComplexResultHelpers = () => { return { caseId: ComplexResult.Tag.Coordinates, cleanup }; } case ComplexResult.Tag.Comprehensive: { - const bytes_8 = textEncoder.encode(value.param8); - const bytesId_8 = swift.memory.retain(bytes_8); - tmpParamInts.push(bytes_8.length); - tmpParamInts.push(bytesId_8); - const bytes_7 = textEncoder.encode(value.param7); - const bytesId_7 = swift.memory.retain(bytes_7); - tmpParamInts.push(bytes_7.length); - tmpParamInts.push(bytesId_7); - const bytes_6 = textEncoder.encode(value.param6); - const bytesId_6 = swift.memory.retain(bytes_6); - tmpParamInts.push(bytes_6.length); - tmpParamInts.push(bytesId_6); + const bytes = textEncoder.encode(value.param8); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); + const bytes1 = textEncoder.encode(value.param7); + const id1 = swift.memory.retain(bytes1); + tmpParamInts.push(bytes1.length); + tmpParamInts.push(id1); + const bytes2 = textEncoder.encode(value.param6); + const id2 = swift.memory.retain(bytes2); + tmpParamInts.push(bytes2.length); + tmpParamInts.push(id2); tmpParamF64s.push(value.param5); tmpParamF64s.push(value.param4); tmpParamInts.push((value.param3 | 0)); @@ -162,9 +162,9 @@ const __bjs_createComplexResultHelpers = () => { tmpParamInts.push(value.param1 ? 1 : 0); tmpParamInts.push(value.param0 ? 1 : 0); const cleanup = () => { - swift.memory.release(bytesId_8); - swift.memory.release(bytesId_7); - swift.memory.release(bytesId_6); + swift.memory.release(id); + swift.memory.release(id1); + swift.memory.release(id2); }; return { caseId: ComplexResult.Tag.Comprehensive, cleanup }; } @@ -179,37 +179,37 @@ const __bjs_createComplexResultHelpers = () => { const tag = tmpRetTag | 0; switch (tag) { case ComplexResult.Tag.Success: { - const string_0 = tmpRetStrings.pop(); - return { tag: ComplexResult.Tag.Success, param0: string_0 }; + const string = tmpRetStrings.pop(); + return { tag: ComplexResult.Tag.Success, param0: string }; } case ComplexResult.Tag.Error: { - const int_1 = tmpRetInts.pop(); - const string_0 = tmpRetStrings.pop(); - return { tag: ComplexResult.Tag.Error, param0: string_0, param1: int_1 }; + const int = tmpRetInts.pop(); + const string = tmpRetStrings.pop(); + return { tag: ComplexResult.Tag.Error, param0: string, param1: int }; } case ComplexResult.Tag.Status: { - const string_2 = tmpRetStrings.pop(); - const int_1 = tmpRetInts.pop(); - const bool_0 = tmpRetInts.pop(); - return { tag: ComplexResult.Tag.Status, param0: bool_0, param1: int_1, param2: string_2 }; + const string = tmpRetStrings.pop(); + const int = tmpRetInts.pop(); + const bool = tmpRetInts.pop(); + return { tag: ComplexResult.Tag.Status, param0: bool, param1: int, param2: string }; } case ComplexResult.Tag.Coordinates: { - const f64_2 = tmpRetF64s.pop(); - const f64_1 = tmpRetF64s.pop(); - const f64_0 = tmpRetF64s.pop(); - return { tag: ComplexResult.Tag.Coordinates, param0: f64_0, param1: f64_1, param2: f64_2 }; + const f64 = tmpRetF64s.pop(); + const f641 = tmpRetF64s.pop(); + const f642 = tmpRetF64s.pop(); + return { tag: ComplexResult.Tag.Coordinates, param0: f642, param1: f641, param2: f64 }; } case ComplexResult.Tag.Comprehensive: { - const string_8 = tmpRetStrings.pop(); - const string_7 = tmpRetStrings.pop(); - const string_6 = tmpRetStrings.pop(); - const f64_5 = tmpRetF64s.pop(); - const f64_4 = tmpRetF64s.pop(); - const int_3 = tmpRetInts.pop(); - const int_2 = tmpRetInts.pop(); - const bool_1 = tmpRetInts.pop(); - const bool_0 = tmpRetInts.pop(); - return { tag: ComplexResult.Tag.Comprehensive, param0: bool_0, param1: bool_1, param2: int_2, param3: int_3, param4: f64_4, param5: f64_5, param6: string_6, param7: string_7, param8: string_8 }; + const string = tmpRetStrings.pop(); + const string1 = tmpRetStrings.pop(); + const string2 = tmpRetStrings.pop(); + const f64 = tmpRetF64s.pop(); + const f641 = tmpRetF64s.pop(); + const int = tmpRetInts.pop(); + const int1 = tmpRetInts.pop(); + const bool = tmpRetInts.pop(); + const bool1 = tmpRetInts.pop(); + return { tag: ComplexResult.Tag.Comprehensive, param0: bool1, param1: bool, param2: int1, param3: int, param4: f641, param5: f64, param6: string2, param7: string1, param8: string }; } case ComplexResult.Tag.Info: return { tag: ComplexResult.Tag.Info }; default: throw new Error("Unknown ComplexResult tag returned from Swift: " + String(tag)); @@ -231,35 +231,35 @@ const __bjs_createResultHelpers = () => { const enumTag = value.tag; switch (enumTag) { case Result.Tag.Success: { - const bytes_0 = textEncoder.encode(value.param0); - const bytesId_0 = swift.memory.retain(bytes_0); - tmpParamInts.push(bytes_0.length); - tmpParamInts.push(bytesId_0); + const bytes = textEncoder.encode(value.param0); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); const cleanup = () => { - swift.memory.release(bytesId_0); + swift.memory.release(id); }; return { caseId: Result.Tag.Success, cleanup }; } case Result.Tag.Failure: { tmpParamInts.push((value.param1 | 0)); - const bytes_0 = textEncoder.encode(value.param0); - const bytesId_0 = swift.memory.retain(bytes_0); - tmpParamInts.push(bytes_0.length); - tmpParamInts.push(bytesId_0); + const bytes = textEncoder.encode(value.param0); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); const cleanup = () => { - swift.memory.release(bytesId_0); + swift.memory.release(id); }; return { caseId: Result.Tag.Failure, cleanup }; } case Result.Tag.Status: { - const bytes_2 = textEncoder.encode(value.param2); - const bytesId_2 = swift.memory.retain(bytes_2); - tmpParamInts.push(bytes_2.length); - tmpParamInts.push(bytesId_2); + const bytes = textEncoder.encode(value.param2); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); tmpParamInts.push((value.param1 | 0)); tmpParamInts.push(value.param0 ? 1 : 0); const cleanup = () => { - swift.memory.release(bytesId_2); + swift.memory.release(id); }; return { caseId: Result.Tag.Status, cleanup }; } @@ -270,19 +270,19 @@ const __bjs_createResultHelpers = () => { const tag = tmpRetTag | 0; switch (tag) { case Result.Tag.Success: { - const string_0 = tmpRetStrings.pop(); - return { tag: Result.Tag.Success, param0: string_0 }; + const string = tmpRetStrings.pop(); + return { tag: Result.Tag.Success, param0: string }; } case Result.Tag.Failure: { - const int_1 = tmpRetInts.pop(); - const string_0 = tmpRetStrings.pop(); - return { tag: Result.Tag.Failure, param0: string_0, param1: int_1 }; + const int = tmpRetInts.pop(); + const string = tmpRetStrings.pop(); + return { tag: Result.Tag.Failure, param0: string, param1: int }; } case Result.Tag.Status: { - const string_2 = tmpRetStrings.pop(); - const int_1 = tmpRetInts.pop(); - const bool_0 = tmpRetInts.pop(); - return { tag: Result.Tag.Status, param0: bool_0, param1: int_1, param2: string_2 }; + const string = tmpRetStrings.pop(); + const int = tmpRetInts.pop(); + const bool = tmpRetInts.pop(); + return { tag: Result.Tag.Status, param0: bool, param1: int, param2: string }; } default: throw new Error("Unknown Result tag returned from Swift: " + String(tag)); } @@ -302,23 +302,23 @@ const __bjs_createNetworkingResultHelpers = () => { const enumTag = value.tag; switch (enumTag) { case NetworkingResult.Tag.Success: { - const bytes_0 = textEncoder.encode(value.param0); - const bytesId_0 = swift.memory.retain(bytes_0); - tmpParamInts.push(bytes_0.length); - tmpParamInts.push(bytesId_0); + const bytes = textEncoder.encode(value.param0); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); const cleanup = () => { - swift.memory.release(bytesId_0); + swift.memory.release(id); }; return { caseId: NetworkingResult.Tag.Success, cleanup }; } case NetworkingResult.Tag.Failure: { tmpParamInts.push((value.param1 | 0)); - const bytes_0 = textEncoder.encode(value.param0); - const bytesId_0 = swift.memory.retain(bytes_0); - tmpParamInts.push(bytes_0.length); - tmpParamInts.push(bytesId_0); + const bytes = textEncoder.encode(value.param0); + const id = swift.memory.retain(bytes); + tmpParamInts.push(bytes.length); + tmpParamInts.push(id); const cleanup = () => { - swift.memory.release(bytesId_0); + swift.memory.release(id); }; return { caseId: NetworkingResult.Tag.Failure, cleanup }; } @@ -329,13 +329,13 @@ const __bjs_createNetworkingResultHelpers = () => { const tag = tmpRetTag | 0; switch (tag) { case NetworkingResult.Tag.Success: { - const string_0 = tmpRetStrings.pop(); - return { tag: NetworkingResult.Tag.Success, param0: string_0 }; + const string = tmpRetStrings.pop(); + return { tag: NetworkingResult.Tag.Success, param0: string }; } case NetworkingResult.Tag.Failure: { - const int_1 = tmpRetInts.pop(); - const string_0 = tmpRetStrings.pop(); - return { tag: NetworkingResult.Tag.Failure, param0: string_0, param1: int_1 }; + const int = tmpRetInts.pop(); + const string = tmpRetStrings.pop(); + return { tag: NetworkingResult.Tag.Failure, param0: string, param1: int }; } default: throw new Error("Unknown NetworkingResult tag returned from Swift: " + String(tag)); } From 95f68690e7771c07c7614b75fc199c6e74a368a4 Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Mon, 1 Sep 2025 12:24:24 +0200 Subject: [PATCH 2/4] BridgeJS: Move whole push into Intrinsic fragment --- .../Sources/BridgeJSLink/BridgeJSLink.swift | 28 ++---------- .../Sources/BridgeJSLink/JSGlueGen.swift | 43 +++++++++++++++++-- .../BridgeJSSkeleton/BridgeJSSkeleton.swift | 11 +++-- 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 59088974..d4c5b5ac 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -700,36 +700,14 @@ struct BridgeJSLink { } else { let scope = JSGlueVariableScope() let cleanup = CodeFragmentPrinter() - cleanup.indent() - cleanup.indent() - cleanup.indent() - cleanup.indent() - cleanup.indent() - cleanup.indent() let printer = CodeFragmentPrinter() + cleanup.indent() - let reversedValues = enumCase.associatedValues.enumerated().reversed() - - for (associatedValueIndex, associatedValue) in reversedValues { - let prop = associatedValue.label ?? "param\(associatedValueIndex)" - let fragment = IntrinsicJSFragment.associatedValuePushPayload(type: associatedValue.type) - - _ = fragment.printCode(["value.\(prop)"], scope, printer, cleanup) - } + let fragment = IntrinsicJSFragment.associatedValuePushPayload(enumCase: enumCase) + _ = fragment.printCode(["value", enumDefinition.name, caseName], scope, printer, cleanup) jsLines.append("case \(enumDefinition.name).Tag.\(caseName): {".indent(count: 16)) jsLines.append(contentsOf: printer.lines.map { $0.indent(count: 20) }) - if cleanup.lines.isEmpty { - jsLines.append("const cleanup = undefined;".indent(count: 20)) - } else { - jsLines.append("const cleanup = () => {".indent(count: 20)) - jsLines.append(contentsOf: cleanup.lines) - jsLines.append("};".indent(count: 20)) - } - jsLines.append( - "return { caseId: \(enumDefinition.name).Tag.\(caseName), cleanup };" - .indent(count: 20) - ) jsLines.append("}".indent(count: 16)) } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift index 3875f0f0..d5cc5a50 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift @@ -241,6 +241,45 @@ struct IntrinsicJSFragment: Sendable { // MARK: - Associated Value Payload Fragments + /// Fragment for pushing all associated value payloads for an entire enum case during enum lowering + static func associatedValuePushPayload(enumCase: EnumCase) -> IntrinsicJSFragment { + return IntrinsicJSFragment( + parameters: ["value", "enumName", "caseName"], + printCode: { arguments, scope, printer, cleanup in + let enumName = arguments[1] + let caseName = arguments[2] + + // If no associated values, return early + if enumCase.associatedValues.isEmpty { + printer.write("const cleanup = undefined;") + printer.write("return { caseId: \(enumName).Tag.\(caseName), cleanup };") + return [] + } + + // Process associated values in reverse order (to match the order they'll be popped) + let reversedValues = enumCase.associatedValues.enumerated().reversed() + + for (associatedValueIndex, associatedValue) in reversedValues { + let prop = associatedValue.label ?? "param\(associatedValueIndex)" + let fragment = IntrinsicJSFragment.associatedValuePushPayload(type: associatedValue.type) + + _ = fragment.printCode(["value.\(prop)"], scope, printer, cleanup) + } + + if cleanup.lines.isEmpty { + printer.write("const cleanup = undefined;") + } else { + printer.write("const cleanup = () => {") + printer.write(contentsOf: cleanup) + printer.write("};") + } + printer.write("return { caseId: \(enumName).Tag.\(caseName), cleanup };") + + return [] + } + ) + } + /// Fragment for pushing associated value payloads during enum lowering static func associatedValuePushPayload(type: BridgeType) -> IntrinsicJSFragment { switch type { @@ -292,11 +331,9 @@ struct IntrinsicJSFragment: Sendable { } ) default: - // For unsupported types, push a default value return IntrinsicJSFragment( - parameters: ["value"], + parameters: [], printCode: { arguments, scope, printer, cleanup in - printer.write("\(JSGlueVariableScope.reservedTmpParamInts).push(0);") return [] } ) diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index 5fa0acb9..50810e58 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -2,7 +2,7 @@ // MARK: - Types -public enum BridgeType: Codable, Equatable { +public enum BridgeType: Codable, Equatable, Sendable { case int, float, double, string, bool, jsObject(String?), swiftHeapObject(String), void case caseEnum(String) case rawValueEnum(String, SwiftEnumRawType) @@ -14,7 +14,7 @@ public enum WasmCoreType: String, Codable, Sendable { case i32, i64, f32, f64, pointer } -public enum SwiftEnumRawType: String, CaseIterable, Codable { +public enum SwiftEnumRawType: String, CaseIterable, Codable, Sendable { case string = "String" case bool = "Bool" case int = "Int" @@ -70,7 +70,7 @@ public struct Effects: Codable { // MARK: - Enum Skeleton -public struct AssociatedValue: Codable, Equatable { +public struct AssociatedValue: Codable, Equatable, Sendable { public let label: String? public let type: BridgeType @@ -80,7 +80,7 @@ public struct AssociatedValue: Codable, Equatable { } } -public struct EnumCase: Codable, Equatable { +public struct EnumCase: Codable, Equatable, Sendable { public let name: String public let rawValue: String? public let associatedValues: [AssociatedValue] @@ -88,8 +88,7 @@ public struct EnumCase: Codable, Equatable { public var isSimple: Bool { associatedValues.isEmpty } - - public init(name: String, rawValue: String?, associatedValues: [AssociatedValue]) { + public init(name: String, rawValue: String?, associatedValues: [AssociatedValue] = []) { self.name = name self.rawValue = rawValue self.associatedValues = associatedValues From ee6b207b41d3e70c09a2dae9689ee3970ad1e684 Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Mon, 1 Sep 2025 13:50:35 +0200 Subject: [PATCH 3/4] BridgeJS: Move JS code generation to helpers --- .../Sources/BridgeJSLink/BridgeJSLink.swift | 337 ++++-------- .../Sources/BridgeJSLink/JSGlueGen.swift | 488 ++++++++++++------ .../BridgeJSSkeleton/BridgeJSSkeleton.swift | 21 +- 3 files changed, 459 insertions(+), 387 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index d4c5b5ac..6c0928cd 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -554,232 +554,118 @@ struct BridgeJSLink { func renderExportedEnum(_ enumDefinition: ExportedEnum) throws -> (js: [String], dts: [String]) { var jsLines: [String] = [] var dtsLines: [String] = [] - let style: EnumEmitStyle = enumDefinition.emitStyle + let scope = JSGlueVariableScope() + let cleanup = CodeFragmentPrinter() + let printer = CodeFragmentPrinter() switch enumDefinition.enumType { case .simple: - jsLines.append("const \(enumDefinition.name) = {") - for (index, enumCase) in enumDefinition.cases.enumerated() { - let caseName = enumCase.name.capitalizedFirstLetter - jsLines.append("\(caseName): \(index),".indent(count: 4)) - } - jsLines.append("};") - jsLines.append("") - - if enumDefinition.namespace == nil { - switch style { - case .tsEnum: - dtsLines.append("export enum \(enumDefinition.name) {") - for (index, enumCase) in enumDefinition.cases.enumerated() { - let caseName = enumCase.name.capitalizedFirstLetter - dtsLines.append("\(caseName) = \(index),".indent(count: 4)) - } - dtsLines.append("}") - dtsLines.append("") - case .const: - dtsLines.append("export const \(enumDefinition.name): {") - for (index, enumCase) in enumDefinition.cases.enumerated() { - let caseName = enumCase.name.capitalizedFirstLetter - dtsLines.append("readonly \(caseName): \(index);".indent(count: 4)) - } - dtsLines.append("};") - dtsLines.append( - "export type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" - ) - dtsLines.append("") - } - } + let fragment = IntrinsicJSFragment.simpleEnumHelper(enumDefinition: enumDefinition) + _ = fragment.printCode([enumDefinition.name], scope, printer, cleanup) + + jsLines.append(contentsOf: printer.lines) case .rawValue: - guard let rawType = enumDefinition.rawType else { + guard enumDefinition.rawType != nil else { throw BridgeJSLinkError(message: "Raw value enum \(enumDefinition.name) is missing rawType") } - jsLines.append("const \(enumDefinition.name) = {") - for enumCase in enumDefinition.cases { - let caseName = enumCase.name.capitalizedFirstLetter - let rawValue = enumCase.rawValue ?? enumCase.name - let formattedValue: String - - if let rawTypeEnum = SwiftEnumRawType.from(rawType) { - switch rawTypeEnum { - case .string: - formattedValue = "\"\(rawValue)\"" - case .bool: - formattedValue = rawValue.lowercased() == "true" ? "true" : "false" - case .float, .double: - formattedValue = rawValue - default: - formattedValue = rawValue - } - } else { - formattedValue = rawValue - } + let fragment = IntrinsicJSFragment.rawValueEnumHelper(enumDefinition: enumDefinition) + _ = fragment.printCode([enumDefinition.name], scope, printer, cleanup) - jsLines.append("\(caseName): \(formattedValue),".indent(count: 4)) - } - jsLines.append("};") - jsLines.append("") - - if enumDefinition.namespace == nil { - switch style { - case .tsEnum: - dtsLines.append("export enum \(enumDefinition.name) {") - for enumCase in enumDefinition.cases { - let caseName = enumCase.name.capitalizedFirstLetter + jsLines.append(contentsOf: printer.lines) + case .associatedValue: + let fragment = IntrinsicJSFragment.associatedValueEnumHelper(enumDefinition: enumDefinition) + _ = fragment.printCode([enumDefinition.name], scope, printer, cleanup) + + jsLines.append(contentsOf: printer.lines) + case .namespace: + break + } + + if enumDefinition.namespace == nil { + dtsLines.append(contentsOf: generateDeclarations(enumDefinition: enumDefinition)) + } + + return (jsLines, dtsLines) + } + + private func generateDeclarations(enumDefinition: ExportedEnum) -> [String] { + let printer = CodeFragmentPrinter() + + switch enumDefinition.emitStyle { + case .tsEnum: + switch enumDefinition.enumType { + case .simple, .rawValue: + printer.write("export enum \(enumDefinition.name) {") + printer.indent() + for (index, enumCase) in enumDefinition.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + let value: String + + switch enumDefinition.enumType { + case .simple: + value = "\(index)" + case .rawValue: let rawValue = enumCase.rawValue ?? enumCase.name - let formattedValue: String - switch rawType { - case "String": formattedValue = "\"\(rawValue)\"" - case "Bool": formattedValue = rawValue.lowercased() == "true" ? "true" : "false" - case "Float", "Double": formattedValue = rawValue - default: formattedValue = rawValue - } - dtsLines.append("\(caseName) = \(formattedValue),".indent(count: 4)) + value = SwiftEnumRawType.formatValue(rawValue, rawType: enumDefinition.rawType ?? "") + case .associatedValue, .namespace: + continue } - dtsLines.append("}") - dtsLines.append("") - case .const: - dtsLines.append("export const \(enumDefinition.name): {") - for enumCase in enumDefinition.cases { - let caseName = enumCase.name.capitalizedFirstLetter - let rawValue = enumCase.rawValue ?? enumCase.name - let formattedValue: String - - switch rawType { - case "String": - formattedValue = "\"\(rawValue)\"" - case "Bool": - formattedValue = rawValue.lowercased() == "true" ? "true" : "false" - case "Float", "Double": - formattedValue = rawValue - default: - formattedValue = rawValue - } - dtsLines.append("readonly \(caseName): \(formattedValue);".indent(count: 4)) - } - dtsLines.append("};") - dtsLines.append( - "export type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" - ) - dtsLines.append("") + printer.write("\(caseName) = \(value),") } + printer.unindent() + printer.write("}") + printer.write("") + case .associatedValue, .namespace: + break } - case .associatedValue: - // Use the new IntrinsicJSFragment for associated value payload handling - jsLines.append("const \(enumDefinition.name) = {") - jsLines.append("Tag: {".indent(count: 4)) - for (index, enumCase) in enumDefinition.cases.enumerated() { - let caseName = enumCase.name.capitalizedFirstLetter - jsLines.append("\(caseName): \(index),".indent(count: 8)) - } - jsLines.append("}".indent(count: 4)) - jsLines.append("};") - jsLines.append("") - jsLines.append("const __bjs_create\(enumDefinition.name)Helpers = () => {") - jsLines.append( - "return (\(JSGlueVariableScope.reservedTmpParamInts), \(JSGlueVariableScope.reservedTmpParamF32s), \(JSGlueVariableScope.reservedTmpParamF64s), textEncoder, \(JSGlueVariableScope.reservedSwift)) => ({" - .indent( - count: 4 - ) - ) - jsLines.append("lower: (value) => {".indent(count: 8)) - jsLines.append("const enumTag = value.tag;".indent(count: 12)) - jsLines.append("switch (enumTag) {".indent(count: 12)) - enumDefinition.cases.forEach { enumCase in - let caseName = enumCase.name.capitalizedFirstLetter - if enumCase.associatedValues.isEmpty { - jsLines.append("case \(enumDefinition.name).Tag.\(caseName): {".indent(count: 16)) - jsLines.append("const cleanup = undefined;".indent(count: 20)) - jsLines.append( - "return { caseId: \(enumDefinition.name).Tag.\(caseName), cleanup };" - .indent(count: 20) - ) - jsLines.append("}".indent(count: 16)) - } else { - let scope = JSGlueVariableScope() - let cleanup = CodeFragmentPrinter() - let printer = CodeFragmentPrinter() - cleanup.indent() - - let fragment = IntrinsicJSFragment.associatedValuePushPayload(enumCase: enumCase) - _ = fragment.printCode(["value", enumDefinition.name, caseName], scope, printer, cleanup) - - jsLines.append("case \(enumDefinition.name).Tag.\(caseName): {".indent(count: 16)) - jsLines.append(contentsOf: printer.lines.map { $0.indent(count: 20) }) - jsLines.append("}".indent(count: 16)) + case .const: + switch enumDefinition.enumType { + case .simple: + printer.write("export const \(enumDefinition.name): {") + printer.indent() + for (index, enumCase) in enumDefinition.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + printer.write("readonly \(caseName): \(index);") } - } - jsLines.append( - "default: throw new Error(\"Unknown \(enumDefinition.name) tag: \" + String(enumTag));".indent( - count: 16 + printer.unindent() + printer.write("};") + printer.write( + "export type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" ) - ) - jsLines.append("}".indent(count: 12)) - jsLines.append("},".indent(count: 8)) - - jsLines.append( - "raise: (\(JSGlueVariableScope.reservedTmpRetTag), \(JSGlueVariableScope.reservedTmpRetStrings), \(JSGlueVariableScope.reservedTmpRetInts), \(JSGlueVariableScope.reservedTmpRetF32s), \(JSGlueVariableScope.reservedTmpRetF64s)) => {" - .indent( - count: 8 - ) - ) - jsLines.append("const tag = tmpRetTag | 0;".indent(count: 12)) - jsLines.append("switch (tag) {".indent(count: 12)) - enumDefinition.cases.forEach { enumCase in - let caseName = enumCase.name.capitalizedFirstLetter - if enumCase.associatedValues.isEmpty { - jsLines.append( - "case \(enumDefinition.name).Tag.\(caseName): return { tag: \(enumDefinition.name).Tag.\(caseName) };" - .indent(count: 16) - ) - } else { - var fieldPairs: [String] = [] - let scope = JSGlueVariableScope() - let printer = CodeFragmentPrinter() - let cleanup = CodeFragmentPrinter() - // Use the new IntrinsicJSFragment for associated value payload handling - for (associatedValueIndex, associatedValue) in enumCase.associatedValues.enumerated().reversed() { - let prop = associatedValue.label ?? "param\(associatedValueIndex)" - let fragment = IntrinsicJSFragment.associatedValuePopPayload(type: associatedValue.type) - - let result = fragment.printCode([], scope, printer, cleanup) - let varName = result.first ?? "value_\(associatedValueIndex)" - - fieldPairs.append("\(prop): \(varName)") - } - - jsLines.append("case \(enumDefinition.name).Tag.\(caseName): {".indent(count: 16)) - jsLines.append(contentsOf: printer.lines.map { $0.indent(count: 20) }) - jsLines.append( - "return { tag: \(enumDefinition.name).Tag.\(caseName), \(fieldPairs.reversed().joined(separator: ", ")) };" - .indent(count: 20) - ) - jsLines.append("}".indent(count: 16)) + printer.write("") + case .rawValue: + printer.write("export const \(enumDefinition.name): {") + printer.indent() + for enumCase in enumDefinition.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let rawValue = enumCase.rawValue ?? enumCase.name + let formattedValue = SwiftEnumRawType.formatValue(rawValue, rawType: enumDefinition.rawType ?? "") + printer.write("readonly \(caseName): \(formattedValue);") } - } - jsLines.append( - "default: throw new Error(\"Unknown \(enumDefinition.name) tag returned from Swift: \" + String(tag));" - .indent( - count: 16 - ) - ) - jsLines.append("}".indent(count: 12)) - jsLines.append("}".indent(count: 8)) - jsLines.append("});".indent(count: 4)) - jsLines.append("};") - - if enumDefinition.namespace == nil { - dtsLines.append("export const \(enumDefinition.name): {") - dtsLines.append("readonly Tag: {".indent(count: 4)) + printer.unindent() + printer.write("};") + printer.write( + "export type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" + ) + printer.write("") + case .associatedValue: + printer.write("export const \(enumDefinition.name): {") + printer.indent() + printer.write("readonly Tag: {") + printer.indent() for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter - dtsLines.append("readonly \(caseName): \(index);".indent(count: 8)) + printer.write("readonly \(caseName): \(index);") } - dtsLines.append("};".indent(count: 4)) - dtsLines.append("};") - dtsLines.append("") + printer.unindent() + printer.write("};") + printer.unindent() + printer.write("};") + printer.write("") + var unionParts: [String] = [] for enumCase in enumDefinition.cases { if enumCase.associatedValues.isEmpty { @@ -790,32 +676,26 @@ struct BridgeJSLink { var fields: [String] = [ "tag: typeof \(enumDefinition.name).Tag.\(enumCase.name.capitalizedFirstLetter)" ] - for (associatedValueIndex, associatedValue) in enumCase.associatedValues - .enumerated() - { + for (associatedValueIndex, associatedValue) in enumCase.associatedValues.enumerated() { let prop = associatedValue.label ?? "param\(associatedValueIndex)" - let ts: String - switch associatedValue.type { - case .string: ts = "string" - case .bool: ts = "boolean" - case .int, .float, .double: ts = "number" - default: ts = "never" - } - fields.append("\(prop): \(ts)") + fields.append("\(prop): \(associatedValue.type.tsType)") } unionParts.append("{ \(fields.joined(separator: "; ")) }") } } - dtsLines.append("export type \(enumDefinition.name) =") - dtsLines.append(" " + unionParts.joined(separator: " | ")) - dtsLines.append("") + + printer.write("export type \(enumDefinition.name) =") + printer.write(" " + unionParts.joined(separator: " | ")) + printer.write("") + case .namespace: + break } - case .namespace: - break } - - return (jsLines, dtsLines) + return printer.lines } +} + +extension BridgeJSLink { func renderExportedFunction(function: ExportedFunction) throws -> (js: [String], dts: [String]) { let thunkBuilder = ExportedThunkBuilder(effects: function.effects) @@ -1502,14 +1382,7 @@ struct BridgeJSLink { .enumerated() { let prop = associatedValue.label ?? "param\(associatedValueIndex)" - let ts: String - switch associatedValue.type { - case .string: ts = "string" - case .bool: ts = "boolean" - case .int, .float, .double: ts = "number" - default: ts = "never" - } - fields.append("\(prop): \(ts)") + fields.append("\(prop): \(associatedValue.type.tsType)") } unionParts.append("{ \(fields.joined(separator: "; ")) }") } diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift index d5cc5a50..0dd515a9 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift @@ -239,49 +239,354 @@ struct IntrinsicJSFragment: Sendable { ) } - // MARK: - Associated Value Payload Fragments + // MARK: - ExportSwift + + /// Returns a fragment that lowers a JS value to Wasm core values for parameters + static func lowerParameter(type: BridgeType) throws -> IntrinsicJSFragment { + switch type { + case .int, .float, .double, .bool: return .identity + case .string: return .stringLowerParameter + case .jsObject: return .jsObjectLowerParameter + case .swiftHeapObject: + return .swiftHeapObjectLowerParameter + case .void: return .void + case .caseEnum: return .identity + case .rawValueEnum(_, let rawType): + switch rawType { + case .string: return .stringLowerParameter + default: return .identity + } + case .associatedValueEnum(let fullName): + let base = fullName.components(separatedBy: ".").last ?? fullName + return .associatedEnumLowerParameter(enumBase: base) + case .namespaceEnum(let string): + throw BridgeJSLinkError(message: "Namespace enums are not supported to be passed as parameters: \(string)") + } + } + + /// Returns a fragment that lifts a Wasm core value to a JS value for return values + static func liftReturn(type: BridgeType) throws -> IntrinsicJSFragment { + switch type { + case .int, .float, .double: return .identity + case .bool: return .boolLiftReturn + case .string: return .stringLiftReturn + case .jsObject: return .jsObjectLiftReturn + case .swiftHeapObject(let name): return .swiftHeapObjectLiftReturn(name) + case .void: return .void + case .caseEnum: return .identity + case .rawValueEnum(_, let rawType): + switch rawType { + case .string: return .stringLiftReturn + case .bool: return .boolLiftReturn + default: return .identity + } + case .associatedValueEnum(let fullName): + let base = fullName.components(separatedBy: ".").last ?? fullName + return .associatedEnumLiftReturn(enumBase: base) + case .namespaceEnum(let string): + throw BridgeJSLinkError( + message: "Namespace enums are not supported to be returned from functions: \(string)" + ) + } + } + + // MARK: - ImportedJS + + /// Returns a fragment that lifts Wasm core values to JS values for parameters + static func liftParameter(type: BridgeType) throws -> IntrinsicJSFragment { + switch type { + case .int, .float, .double: return .identity + case .bool: return .boolLiftParameter + case .string: return .stringLiftParameter + case .jsObject: return .jsObjectLiftParameter + case .swiftHeapObject(let name): + throw BridgeJSLinkError( + message: + "Swift heap objects are not supported to be passed as parameters to imported JS functions: \(name)" + ) + case .void: + throw BridgeJSLinkError( + message: "Void can't appear in parameters of imported JS functions" + ) + case .caseEnum: return .identity + case .rawValueEnum(_, let rawType): + switch rawType { + case .string: return .stringLiftParameter + case .bool: return .boolLiftParameter + default: return .identity + } + case .associatedValueEnum(let string): + throw BridgeJSLinkError( + message: + "Associated value enums are not supported to be passed as parameters to imported JS functions: \(string)" + ) + case .namespaceEnum(let string): + throw BridgeJSLinkError( + message: + "Namespace enums are not supported to be passed as parameters to imported JS functions: \(string)" + ) + } + } + + /// Returns a fragment that lowers a JS value to Wasm core values for return values + static func lowerReturn(type: BridgeType) throws -> IntrinsicJSFragment { + switch type { + case .int, .float, .double: return .identity + case .bool: return .boolLowerReturn + case .string: return .stringLowerReturn + case .jsObject: return .jsObjectLowerReturn + case .swiftHeapObject: + throw BridgeJSLinkError( + message: "Swift heap objects are not supported to be returned from imported JS functions" + ) + case .void: return .void + case .caseEnum: return .identity + case .rawValueEnum(_, let rawType): + switch rawType { + case .string: return .stringLowerReturn + case .bool: return .boolLowerReturn + default: return .identity + } + case .associatedValueEnum(let string): + throw BridgeJSLinkError( + message: "Associated value enums are not supported to be returned from imported JS functions: \(string)" + ) + case .namespaceEnum(let string): + throw BridgeJSLinkError( + message: "Namespace enums are not supported to be returned from imported JS functions: \(string)" + ) + } + } + + // MARK: - Enums Payload Fragments - /// Fragment for pushing all associated value payloads for an entire enum case during enum lowering - static func associatedValuePushPayload(enumCase: EnumCase) -> IntrinsicJSFragment { + /// Fragment for generating an entire associated value enum helper + static func associatedValueEnumHelper(enumDefinition: ExportedEnum) -> IntrinsicJSFragment { + return IntrinsicJSFragment( + parameters: ["enumName"], + printCode: { arguments, scope, printer, cleanup in + let enumName = arguments[0] + + // Generate the enum tag object + printer.write("const \(enumName) = {") + printer.indent() + printer.write("Tag: {") + printer.indent() + for (index, enumCase) in enumDefinition.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + printer.write("\(caseName): \(index),") + } + printer.unindent() + printer.write("}") + printer.unindent() + printer.write("};") + printer.write("") + + // Generate the helper function + printer.write("const __bjs_create\(enumName)Helpers = () => {") + printer.indent() + printer.write( + "return (\(JSGlueVariableScope.reservedTmpParamInts), \(JSGlueVariableScope.reservedTmpParamF32s), \(JSGlueVariableScope.reservedTmpParamF64s), textEncoder, \(JSGlueVariableScope.reservedSwift)) => ({" + ) + printer.indent() + + // Generate lower function + printer.write("lower: (value) => {") + printer.indent() + printer.write("const enumTag = value.tag;") + printer.write("switch (enumTag) {") + printer.indent() + + let lowerPrinter = CodeFragmentPrinter() + + for enumCase in enumDefinition.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let caseScope = JSGlueVariableScope() + let caseCleanup = CodeFragmentPrinter() + caseCleanup.indent() + + let fragment = IntrinsicJSFragment.associatedValuePushPayload(enumCase: enumCase) + _ = fragment.printCode(["value", enumName, caseName], caseScope, lowerPrinter, caseCleanup) + } + + for line in lowerPrinter.lines { + printer.write(line) + } + + printer.write("default: throw new Error(\"Unknown \(enumName) tag: \" + String(enumTag));") + printer.unindent() + printer.write("}") + printer.unindent() + printer.write("},") + + // Generate raise function + printer.write( + "raise: (\(JSGlueVariableScope.reservedTmpRetTag), \(JSGlueVariableScope.reservedTmpRetStrings), \(JSGlueVariableScope.reservedTmpRetInts), \(JSGlueVariableScope.reservedTmpRetF32s), \(JSGlueVariableScope.reservedTmpRetF64s)) => {" + ) + printer.indent() + printer.write("const tag = tmpRetTag | 0;") + printer.write("switch (tag) {") + printer.indent() + + let raisePrinter = CodeFragmentPrinter() + + for enumCase in enumDefinition.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let caseScope = JSGlueVariableScope() + let caseCleanup = CodeFragmentPrinter() + + let fragment = IntrinsicJSFragment.associatedValuePopPayload(enumCase: enumCase) + _ = fragment.printCode([enumName, caseName], caseScope, raisePrinter, caseCleanup) + } + + for line in raisePrinter.lines { + printer.write(line) + } + + printer.write( + "default: throw new Error(\"Unknown \(enumName) tag returned from Swift: \" + String(tag));" + ) + printer.unindent() + printer.write("}") + printer.unindent() + printer.write("}") + printer.unindent() + printer.write("});") + printer.unindent() + printer.write("};") + + return [] + } + ) + } + + static func simpleEnumHelper(enumDefinition: ExportedEnum) -> IntrinsicJSFragment { + return IntrinsicJSFragment( + parameters: ["enumName"], + printCode: { arguments, scope, printer, cleanup in + let enumName = arguments[0] + printer.write("const \(enumName) = {") + printer.indent() + for (index, enumCase) in enumDefinition.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + printer.write("\(caseName): \(index),") + } + printer.unindent() + printer.write("};") + printer.write("") + + return [] + } + ) + } + + static func rawValueEnumHelper(enumDefinition: ExportedEnum) -> IntrinsicJSFragment { + return IntrinsicJSFragment( + parameters: ["enumName"], + printCode: { arguments, scope, printer, cleanup in + let enumName = arguments[0] + printer.write("const \(enumName) = {") + printer.indent() + for enumCase in enumDefinition.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let rawValue = enumCase.rawValue ?? enumCase.name + let formattedValue = SwiftEnumRawType.formatValue(rawValue, rawType: enumDefinition.rawType ?? "") + + printer.write("\(caseName): \(formattedValue),") + } + printer.unindent() + printer.write("};") + printer.write("") + + return [] + } + ) + } + + private static func associatedValuePushPayload(enumCase: EnumCase) -> IntrinsicJSFragment { return IntrinsicJSFragment( parameters: ["value", "enumName", "caseName"], printCode: { arguments, scope, printer, cleanup in let enumName = arguments[1] let caseName = arguments[2] - - // If no associated values, return early - if enumCase.associatedValues.isEmpty { - printer.write("const cleanup = undefined;") - printer.write("return { caseId: \(enumName).Tag.\(caseName), cleanup };") - return [] - } - - // Process associated values in reverse order (to match the order they'll be popped) - let reversedValues = enumCase.associatedValues.enumerated().reversed() - - for (associatedValueIndex, associatedValue) in reversedValues { - let prop = associatedValue.label ?? "param\(associatedValueIndex)" - let fragment = IntrinsicJSFragment.associatedValuePushPayload(type: associatedValue.type) - - _ = fragment.printCode(["value.\(prop)"], scope, printer, cleanup) + + printer.write("case \(enumName).Tag.\(caseName): {") + + printer.indent { + if enumCase.associatedValues.isEmpty { + printer.write("const cleanup = undefined;") + printer.write("return { caseId: \(enumName).Tag.\(caseName), cleanup };") + } else { + // Process associated values in reverse order (to match the order they'll be popped) + let reversedValues = enumCase.associatedValues.enumerated().reversed() + + for (associatedValueIndex, associatedValue) in reversedValues { + let prop = associatedValue.label ?? "param\(associatedValueIndex)" + let fragment = IntrinsicJSFragment.associatedValuePushPayload(type: associatedValue.type) + + _ = fragment.printCode(["value.\(prop)"], scope, printer, cleanup) + } + + if cleanup.lines.isEmpty { + printer.write("const cleanup = undefined;") + } else { + printer.write("const cleanup = () => {") + printer.write(contentsOf: cleanup) + printer.write("};") + } + printer.write("return { caseId: \(enumName).Tag.\(caseName), cleanup };") + } } - - if cleanup.lines.isEmpty { - printer.write("const cleanup = undefined;") + + printer.write("}") + + return [] + } + ) + } + + private static func associatedValuePopPayload(enumCase: EnumCase) -> IntrinsicJSFragment { + return IntrinsicJSFragment( + parameters: ["enumName", "caseName"], + printCode: { arguments, scope, printer, cleanup in + let enumName = arguments[0] + let caseName = arguments[1] + + if enumCase.associatedValues.isEmpty { + printer.write("case \(enumName).Tag.\(caseName): return { tag: \(enumName).Tag.\(caseName) };") } else { - printer.write("const cleanup = () => {") - printer.write(contentsOf: cleanup) - printer.write("};") + var fieldPairs: [String] = [] + let casePrinter = CodeFragmentPrinter() + + // Process associated values in reverse order (to match the order they'll be popped) + for (associatedValueIndex, associatedValue) in enumCase.associatedValues.enumerated().reversed() { + let prop = associatedValue.label ?? "param\(associatedValueIndex)" + let fragment = IntrinsicJSFragment.associatedValuePopPayload(type: associatedValue.type) + + let result = fragment.printCode([], scope, casePrinter, cleanup) + let varName = result.first ?? "value_\(associatedValueIndex)" + + fieldPairs.append("\(prop): \(varName)") + } + + printer.write("case \(enumName).Tag.\(caseName): {") + printer.indent { + for line in casePrinter.lines { + printer.write(line) + } + printer.write( + "return { tag: \(enumName).Tag.\(caseName), \(fieldPairs.reversed().joined(separator: ", ")) };" + ) + } + printer.write("}") } - printer.write("return { caseId: \(enumName).Tag.\(caseName), cleanup };") - + return [] } ) } - - /// Fragment for pushing associated value payloads during enum lowering - static func associatedValuePushPayload(type: BridgeType) -> IntrinsicJSFragment { + + private static func associatedValuePushPayload(type: BridgeType) -> IntrinsicJSFragment { switch type { case .string: return IntrinsicJSFragment( @@ -340,8 +645,7 @@ struct IntrinsicJSFragment: Sendable { } } - /// Fragment for popping associated value payloads during enum lifting - static func associatedValuePopPayload(type: BridgeType) -> IntrinsicJSFragment { + private static func associatedValuePopPayload(type: BridgeType) -> IntrinsicJSFragment { switch type { case .string: return IntrinsicJSFragment( @@ -389,7 +693,6 @@ struct IntrinsicJSFragment: Sendable { } ) default: - // For unsupported types, return undefined return IntrinsicJSFragment( parameters: [], printCode: { arguments, scope, printer, cleanup in @@ -398,123 +701,4 @@ struct IntrinsicJSFragment: Sendable { ) } } - - // MARK: - ExportSwift - - /// Returns a fragment that lowers a JS value to Wasm core values for parameters - static func lowerParameter(type: BridgeType) throws -> IntrinsicJSFragment { - switch type { - case .int, .float, .double, .bool: return .identity - case .string: return .stringLowerParameter - case .jsObject: return .jsObjectLowerParameter - case .swiftHeapObject: - return .swiftHeapObjectLowerParameter - case .void: return .void - case .caseEnum: return .identity - case .rawValueEnum(_, let rawType): - switch rawType { - case .string: return .stringLowerParameter - default: return .identity - } - case .associatedValueEnum(let fullName): - let base = fullName.components(separatedBy: ".").last ?? fullName - return .associatedEnumLowerParameter(enumBase: base) - case .namespaceEnum(let string): - throw BridgeJSLinkError(message: "Namespace enums are not supported to be passed as parameters: \(string)") - } - } - - /// Returns a fragment that lifts a Wasm core value to a JS value for return values - static func liftReturn(type: BridgeType) throws -> IntrinsicJSFragment { - switch type { - case .int, .float, .double: return .identity - case .bool: return .boolLiftReturn - case .string: return .stringLiftReturn - case .jsObject: return .jsObjectLiftReturn - case .swiftHeapObject(let name): return .swiftHeapObjectLiftReturn(name) - case .void: return .void - case .caseEnum: return .identity - case .rawValueEnum(_, let rawType): - switch rawType { - case .string: return .stringLiftReturn - case .bool: return .boolLiftReturn - default: return .identity - } - case .associatedValueEnum(let fullName): - let base = fullName.components(separatedBy: ".").last ?? fullName - return .associatedEnumLiftReturn(enumBase: base) - case .namespaceEnum(let string): - throw BridgeJSLinkError( - message: "Namespace enums are not supported to be returned from functions: \(string)" - ) - } - } - - // MARK: - ImportedJS - - /// Returns a fragment that lifts Wasm core values to JS values for parameters - static func liftParameter(type: BridgeType) throws -> IntrinsicJSFragment { - switch type { - case .int, .float, .double: return .identity - case .bool: return .boolLiftParameter - case .string: return .stringLiftParameter - case .jsObject: return .jsObjectLiftParameter - case .swiftHeapObject(let name): - throw BridgeJSLinkError( - message: - "Swift heap objects are not supported to be passed as parameters to imported JS functions: \(name)" - ) - case .void: - throw BridgeJSLinkError( - message: "Void can't appear in parameters of imported JS functions" - ) - case .caseEnum: return .identity - case .rawValueEnum(_, let rawType): - switch rawType { - case .string: return .stringLiftParameter - case .bool: return .boolLiftParameter - default: return .identity - } - case .associatedValueEnum(let string): - throw BridgeJSLinkError( - message: - "Associated value enums are not supported to be passed as parameters to imported JS functions: \(string)" - ) - case .namespaceEnum(let string): - throw BridgeJSLinkError( - message: - "Namespace enums are not supported to be passed as parameters to imported JS functions: \(string)" - ) - } - } - - /// Returns a fragment that lowers a JS value to Wasm core values for return values - static func lowerReturn(type: BridgeType) throws -> IntrinsicJSFragment { - switch type { - case .int, .float, .double: return .identity - case .bool: return .boolLowerReturn - case .string: return .stringLowerReturn - case .jsObject: return .jsObjectLowerReturn - case .swiftHeapObject: - throw BridgeJSLinkError( - message: "Swift heap objects are not supported to be returned from imported JS functions" - ) - case .void: return .void - case .caseEnum: return .identity - case .rawValueEnum(_, let rawType): - switch rawType { - case .string: return .stringLowerReturn - case .bool: return .boolLowerReturn - default: return .identity - } - case .associatedValueEnum(let string): - throw BridgeJSLinkError( - message: "Associated value enums are not supported to be returned from imported JS functions: \(string)" - ) - case .namespaceEnum(let string): - throw BridgeJSLinkError( - message: "Namespace enums are not supported to be returned from imported JS functions: \(string)" - ) - } - } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift index 50810e58..a0d4bd9f 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSSkeleton/BridgeJSSkeleton.swift @@ -44,6 +44,21 @@ public enum SwiftEnumRawType: String, CaseIterable, Codable, Sendable { public static func from(_ rawTypeString: String) -> SwiftEnumRawType? { return Self.allCases.first { $0.rawValue == rawTypeString } } + + public static func formatValue(_ rawValue: String, rawType: String) -> String { + if let enumType = from(rawType) { + switch enumType { + case .string: + return "\"\(rawValue)\"" + case .bool: + return rawValue.lowercased() == "true" ? "true" : "false" + case .float, .double, .int, .int32, .int64, .uint, .uint32, .uint64: + return rawValue + } + } else { + return rawValue + } + } } public struct Parameter: Codable { @@ -95,12 +110,12 @@ public struct EnumCase: Codable, Equatable, Sendable { } } -public enum EnumEmitStyle: String, Codable { +public enum EnumEmitStyle: String, Codable, Sendable { case const case tsEnum } -public struct ExportedEnum: Codable, Equatable { +public struct ExportedEnum: Codable, Equatable, Sendable { public let name: String public let swiftCallName: String public let explicitAccessControl: String? @@ -137,7 +152,7 @@ public struct ExportedEnum: Codable, Equatable { } } -public enum EnumType: String, Codable { +public enum EnumType: String, Codable, Sendable { case simple // enum Direction { case north, south, east } case rawValue // enum Mode: String { case light = "light" } case associatedValue // enum Result { case success(String), failure(Int) } From c71fea245a9ac6e2bb56cf25d104d27807f1559d Mon Sep 17 00:00:00 2001 From: Krzysztof Rodak Date: Mon, 1 Sep 2025 14:12:08 +0200 Subject: [PATCH 4/4] BridgeJS: Migrate some bridgejslink code to code fragment printer --- .../Sources/BridgeJSLink/BridgeJSLink.swift | 196 ++++++++---------- .../Sources/BridgeJSLink/JSGlueGen.swift | 136 ++++++------ 2 files changed, 159 insertions(+), 173 deletions(-) diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift index 6c0928cd..caecef3a 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/BridgeJSLink.swift @@ -358,20 +358,20 @@ struct BridgeJSLink { } private func renderEnumHelperAssignments() -> [String] { - var lines: [String] = [] + let printer = CodeFragmentPrinter() for skeleton in exportedSkeletons { for enumDef in skeleton.enums where enumDef.enumType == .associatedValue { let base = enumDef.name - lines.append( + printer.write( "const \(base)Helpers = __bjs_create\(base)Helpers()(\(JSGlueVariableScope.reservedTmpParamInts), \(JSGlueVariableScope.reservedTmpParamF32s), \(JSGlueVariableScope.reservedTmpParamF64s), \(JSGlueVariableScope.reservedTextEncoder), \(JSGlueVariableScope.reservedSwift));" ) - lines.append("enumHelpers.\(base) = \(base)Helpers;") - lines.append("") + printer.write("enumHelpers.\(base) = \(base)Helpers;") + printer.nextLine() } } - return lines + return printer.lines } private func renderSwiftClassWrappers() -> [String] { @@ -408,18 +408,19 @@ struct BridgeJSLink { } private func generateImportedTypeDefinitions() -> [String] { - var typeDefinitions: [String] = [] + let printer = CodeFragmentPrinter() for skeletonSet in importedSkeletons { for fileSkeleton in skeletonSet.children { for type in fileSkeleton.types { - typeDefinitions.append("export interface \(type.name) {") + printer.write("export interface \(type.name) {") + printer.indent() // Add methods for method in type.methods { let methodSignature = "\(method.name)\(renderTSSignature(parameters: method.parameters, returnType: method.returnType, effects: Effects(isAsync: false, isThrows: false)));" - typeDefinitions.append(methodSignature.indent(count: 4)) + printer.write(methodSignature) } // Add properties @@ -428,15 +429,16 @@ struct BridgeJSLink { property.isReadonly ? "readonly \(property.name): \(property.type.tsType);" : "\(property.name): \(property.type.tsType);" - typeDefinitions.append(propertySignature.indent(count: 4)) + printer.write(propertySignature) } - typeDefinitions.append("}") + printer.unindent() + printer.write("}") } } } - return typeDefinitions + return printer.lines } class ExportedThunkBuilder { @@ -616,7 +618,7 @@ struct BridgeJSLink { } printer.unindent() printer.write("}") - printer.write("") + printer.nextLine() case .associatedValue, .namespace: break } @@ -635,7 +637,7 @@ struct BridgeJSLink { printer.write( "export type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" ) - printer.write("") + printer.nextLine() case .rawValue: printer.write("export const \(enumDefinition.name): {") printer.indent() @@ -650,7 +652,7 @@ struct BridgeJSLink { printer.write( "export type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" ) - printer.write("") + printer.nextLine() case .associatedValue: printer.write("export const \(enumDefinition.name): {") printer.indent() @@ -664,7 +666,7 @@ struct BridgeJSLink { printer.write("};") printer.unindent() printer.write("};") - printer.write("") + printer.nextLine() var unionParts: [String] = [] for enumCase in enumDefinition.cases { @@ -686,7 +688,7 @@ struct BridgeJSLink { printer.write("export type \(enumDefinition.name) =") printer.write(" " + unionParts.joined(separator: " | ")) - printer.write("") + printer.nextLine() case .namespace: break } @@ -1068,7 +1070,7 @@ extension BridgeJSLink { namespacedFunctions: [ExportedFunction], namespacedClasses: [ExportedClass] ) -> [String] { - var lines: [String] = [] + let printer = CodeFragmentPrinter() var uniqueNamespaces: [String] = [] var seen = Set() @@ -1095,28 +1097,30 @@ extension BridgeJSLink { } uniqueNamespaces.sorted().forEach { namespace in - lines.append("if (typeof globalThis.\(namespace) === 'undefined') {") - lines.append("globalThis.\(namespace) = {};".indent(count: 4)) - lines.append("}") + printer.write("if (typeof globalThis.\(namespace) === 'undefined') {") + printer.indent() + printer.write("globalThis.\(namespace) = {};") + printer.unindent() + printer.write("}") } namespacedClasses.forEach { klass in let namespacePath: String = klass.namespace?.joined(separator: ".") ?? "" - lines.append("globalThis.\(namespacePath).\(klass.name) = exports.\(klass.name);") + printer.write("globalThis.\(namespacePath).\(klass.name) = exports.\(klass.name);") } namespacedFunctions.forEach { function in let namespacePath: String = function.namespace?.joined(separator: ".") ?? "" - lines.append("globalThis.\(namespacePath).\(function.name) = exports.\(function.name);") + printer.write("globalThis.\(namespacePath).\(function.name) = exports.\(function.name);") } - return lines + return printer.lines } func renderTopLevelEnumNamespaceAssignments(namespacedEnums: [ExportedEnum]) -> [String] { guard !namespacedEnums.isEmpty else { return [] } - var lines: [String] = [] + let printer = CodeFragmentPrinter() var uniqueNamespaces: [String] = [] var seen = Set() @@ -1132,21 +1136,23 @@ extension BridgeJSLink { } for namespace in uniqueNamespaces { - lines.append("if (typeof globalThis.\(namespace) === 'undefined') {") - lines.append("globalThis.\(namespace) = {};".indent(count: 4)) - lines.append("}") + printer.write("if (typeof globalThis.\(namespace) === 'undefined') {") + printer.indent() + printer.write("globalThis.\(namespace) = {};") + printer.unindent() + printer.write("}") } - if !lines.isEmpty { - lines.append("") + if !uniqueNamespaces.isEmpty { + printer.nextLine() } for enumDef in namespacedEnums { let namespacePath = enumDef.namespace?.joined(separator: ".") ?? "" - lines.append("globalThis.\(namespacePath).\(enumDef.name) = \(enumDef.name);") + printer.write("globalThis.\(namespacePath).\(enumDef.name) = \(enumDef.name);") } - return lines + return printer.lines } private struct NamespaceContent { @@ -1193,7 +1199,7 @@ extension BridgeJSLink { exportedSkeletons: [ExportedSkeleton], renderTSSignatureCallback: @escaping ([Parameter], BridgeType, Effects) -> String ) -> [String] { - var dtsLines: [String] = [] + let printer = CodeFragmentPrinter() let rootNode = NamespaceNode(name: "") @@ -1230,41 +1236,41 @@ extension BridgeJSLink { } guard !rootNode.children.isEmpty else { - return dtsLines + return printer.lines } - dtsLines.append("export {};") - dtsLines.append("") - dtsLines.append("declare global {") - - let identBaseSize = 4 + printer.write("export {};") + printer.nextLine() + printer.write("declare global {") + printer.indent() func generateNamespaceDeclarations(node: NamespaceNode, depth: Int) { let sortedChildren = node.children.sorted { $0.key < $1.key } for (childName, childNode) in sortedChildren { - dtsLines.append("namespace \(childName) {".indent(count: identBaseSize * depth)) - - let contentDepth = depth + 1 + printer.write("namespace \(childName) {") + printer.indent() let sortedClasses = childNode.content.classes.sorted { $0.name < $1.name } for klass in sortedClasses { - dtsLines.append("class \(klass.name) {".indent(count: identBaseSize * contentDepth)) + printer.write("class \(klass.name) {") + printer.indent() if let constructor = klass.constructor { let constructorSignature = "constructor(\(constructor.parameters.map { "\($0.name): \($0.type.tsType)" }.joined(separator: ", ")));" - dtsLines.append("\(constructorSignature)".indent(count: identBaseSize * (contentDepth + 1))) + printer.write(constructorSignature) } let sortedMethods = klass.methods.sorted { $0.name < $1.name } for method in sortedMethods { let methodSignature = "\(method.name)\(renderTSSignatureCallback(method.parameters, method.returnType, method.effects));" - dtsLines.append("\(methodSignature)".indent(count: identBaseSize * (contentDepth + 1))) + printer.write(methodSignature) } - dtsLines.append("}".indent(count: identBaseSize * contentDepth)) + printer.unindent() + printer.write("}") } let sortedEnums = childNode.content.enums.sorted { $0.name < $1.name } @@ -1274,41 +1280,33 @@ extension BridgeJSLink { case .simple: switch style { case .tsEnum: - dtsLines.append( - "enum \(enumDefinition.name) {".indent(count: identBaseSize * contentDepth) - ) + printer.write("enum \(enumDefinition.name) {") + printer.indent() for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter - dtsLines.append( - "\(caseName) = \(index),".indent(count: identBaseSize * (contentDepth + 1)) - ) + printer.write("\(caseName) = \(index),") } - dtsLines.append("}".indent(count: identBaseSize * contentDepth)) + printer.unindent() + printer.write("}") case .const: - dtsLines.append( - "const \(enumDefinition.name): {".indent(count: identBaseSize * contentDepth) - ) + printer.write("const \(enumDefinition.name): {") + printer.indent() for (index, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter - dtsLines.append( - "readonly \(caseName): \(index);".indent( - count: identBaseSize * (contentDepth + 1) - ) - ) + printer.write("readonly \(caseName): \(index);") } - dtsLines.append("};".indent(count: identBaseSize * contentDepth)) - dtsLines.append( + printer.unindent() + printer.write("};") + printer.write( "type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" - .indent(count: identBaseSize * contentDepth) ) } case .rawValue: guard let rawType = enumDefinition.rawType else { continue } switch style { case .tsEnum: - dtsLines.append( - "enum \(enumDefinition.name) {".indent(count: identBaseSize * contentDepth) - ) + printer.write("enum \(enumDefinition.name) {") + printer.indent() for enumCase in enumDefinition.cases { let caseName = enumCase.name.capitalizedFirstLetter let rawValue = enumCase.rawValue ?? enumCase.name @@ -1319,17 +1317,13 @@ extension BridgeJSLink { case "Float", "Double": formattedValue = rawValue default: formattedValue = rawValue } - dtsLines.append( - "\(caseName) = \(formattedValue),".indent( - count: identBaseSize * (contentDepth + 1) - ) - ) + printer.write("\(caseName) = \(formattedValue),") } - dtsLines.append("}".indent(count: identBaseSize * contentDepth)) + printer.unindent() + printer.write("}") case .const: - dtsLines.append( - "const \(enumDefinition.name): {".indent(count: identBaseSize * contentDepth) - ) + printer.write("const \(enumDefinition.name): {") + printer.indent() for enumCase in enumDefinition.cases { let caseName = enumCase.name.capitalizedFirstLetter let rawValue = enumCase.rawValue ?? enumCase.name @@ -1340,33 +1334,27 @@ extension BridgeJSLink { case "Float", "Double": formattedValue = rawValue default: formattedValue = rawValue } - dtsLines.append( - "readonly \(caseName): \(formattedValue);".indent( - count: identBaseSize * (contentDepth + 1) - ) - ) + printer.write("readonly \(caseName): \(formattedValue);") } - dtsLines.append("};".indent(count: identBaseSize * contentDepth)) - dtsLines.append( + printer.unindent() + printer.write("};") + printer.write( "type \(enumDefinition.name) = typeof \(enumDefinition.name)[keyof typeof \(enumDefinition.name)];" - .indent(count: identBaseSize * contentDepth) ) } case .associatedValue: - dtsLines.append( - "const \(enumDefinition.name): {".indent(count: identBaseSize * contentDepth) - ) - dtsLines.append("readonly Tag: {".indent(count: identBaseSize * (contentDepth + 1))) + printer.write("const \(enumDefinition.name): {") + printer.indent() + printer.write("readonly Tag: {") + printer.indent() for (caseIndex, enumCase) in enumDefinition.cases.enumerated() { let caseName = enumCase.name.capitalizedFirstLetter - dtsLines.append( - "readonly \(caseName): \(caseIndex);".indent( - count: identBaseSize * (contentDepth + 2) - ) - ) + printer.write("readonly \(caseName): \(caseIndex);") } - dtsLines.append("};".indent(count: identBaseSize * (contentDepth + 1))) - dtsLines.append("};".indent(count: identBaseSize * contentDepth)) + printer.unindent() + printer.write("};") + printer.unindent() + printer.write("};") var unionParts: [String] = [] for enumCase in enumDefinition.cases { @@ -1387,10 +1375,8 @@ extension BridgeJSLink { unionParts.append("{ \(fields.joined(separator: "; ")) }") } } - dtsLines.append("type \(enumDefinition.name) =".indent(count: identBaseSize * contentDepth)) - dtsLines.append( - " " + unionParts.joined(separator: " | ").indent(count: identBaseSize * contentDepth) - ) + printer.write("type \(enumDefinition.name) =") + printer.write(" " + unionParts.joined(separator: " | ")) case .namespace: continue } @@ -1400,21 +1386,23 @@ extension BridgeJSLink { for function in sortedFunctions { let signature = "\(function.name)\(renderTSSignatureCallback(function.parameters, function.returnType, function.effects));" - dtsLines.append("\(signature)".indent(count: identBaseSize * contentDepth)) + printer.write(signature) } - generateNamespaceDeclarations(node: childNode, depth: contentDepth) + generateNamespaceDeclarations(node: childNode, depth: depth + 1) - dtsLines.append("}".indent(count: identBaseSize * depth)) + printer.unindent() + printer.write("}") } } generateNamespaceDeclarations(node: rootNode, depth: 1) - dtsLines.append("}") - dtsLines.append("") + printer.unindent() + printer.write("}") + printer.nextLine() - return dtsLines + return printer.lines } } diff --git a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift index 0dd515a9..36977aae 100644 --- a/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift +++ b/Plugins/BridgeJS/Sources/BridgeJSLink/JSGlueGen.swift @@ -369,18 +369,18 @@ struct IntrinsicJSFragment: Sendable { // Generate the enum tag object printer.write("const \(enumName) = {") - printer.indent() - printer.write("Tag: {") - printer.indent() - for (index, enumCase) in enumDefinition.cases.enumerated() { - let caseName = enumCase.name.capitalizedFirstLetter - printer.write("\(caseName): \(index),") + printer.indent { + printer.write("Tag: {") + printer.indent { + for (index, enumCase) in enumDefinition.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + printer.write("\(caseName): \(index),") + } + } + printer.write("}") } - printer.unindent() - printer.write("}") - printer.unindent() printer.write("};") - printer.write("") + printer.nextLine() // Generate the helper function printer.write("const __bjs_create\(enumName)Helpers = () => {") @@ -392,63 +392,58 @@ struct IntrinsicJSFragment: Sendable { // Generate lower function printer.write("lower: (value) => {") - printer.indent() - printer.write("const enumTag = value.tag;") - printer.write("switch (enumTag) {") - printer.indent() - - let lowerPrinter = CodeFragmentPrinter() - - for enumCase in enumDefinition.cases { - let caseName = enumCase.name.capitalizedFirstLetter - let caseScope = JSGlueVariableScope() - let caseCleanup = CodeFragmentPrinter() - caseCleanup.indent() + printer.indent { + printer.write("const enumTag = value.tag;") + printer.write("switch (enumTag) {") + printer.indent { + let lowerPrinter = CodeFragmentPrinter() + for enumCase in enumDefinition.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let caseScope = JSGlueVariableScope() + let caseCleanup = CodeFragmentPrinter() + caseCleanup.indent() + let fragment = IntrinsicJSFragment.associatedValuePushPayload(enumCase: enumCase) + _ = fragment.printCode(["value", enumName, caseName], caseScope, lowerPrinter, caseCleanup) + } - let fragment = IntrinsicJSFragment.associatedValuePushPayload(enumCase: enumCase) - _ = fragment.printCode(["value", enumName, caseName], caseScope, lowerPrinter, caseCleanup) - } + for line in lowerPrinter.lines { + printer.write(line) + } - for line in lowerPrinter.lines { - printer.write(line) + printer.write("default: throw new Error(\"Unknown \(enumName) tag: \" + String(enumTag));") + } + printer.write("}") } - - printer.write("default: throw new Error(\"Unknown \(enumName) tag: \" + String(enumTag));") - printer.unindent() - printer.write("}") - printer.unindent() printer.write("},") // Generate raise function printer.write( "raise: (\(JSGlueVariableScope.reservedTmpRetTag), \(JSGlueVariableScope.reservedTmpRetStrings), \(JSGlueVariableScope.reservedTmpRetInts), \(JSGlueVariableScope.reservedTmpRetF32s), \(JSGlueVariableScope.reservedTmpRetF64s)) => {" ) - printer.indent() - printer.write("const tag = tmpRetTag | 0;") - printer.write("switch (tag) {") - printer.indent() - - let raisePrinter = CodeFragmentPrinter() - - for enumCase in enumDefinition.cases { - let caseName = enumCase.name.capitalizedFirstLetter - let caseScope = JSGlueVariableScope() - let caseCleanup = CodeFragmentPrinter() + printer.indent { + printer.write("const tag = tmpRetTag | 0;") + printer.write("switch (tag) {") + printer.indent { + let raisePrinter = CodeFragmentPrinter() + for enumCase in enumDefinition.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let caseScope = JSGlueVariableScope() + let caseCleanup = CodeFragmentPrinter() + + let fragment = IntrinsicJSFragment.associatedValuePopPayload(enumCase: enumCase) + _ = fragment.printCode([enumName, caseName], caseScope, raisePrinter, caseCleanup) + } - let fragment = IntrinsicJSFragment.associatedValuePopPayload(enumCase: enumCase) - _ = fragment.printCode([enumName, caseName], caseScope, raisePrinter, caseCleanup) - } + for line in raisePrinter.lines { + printer.write(line) + } - for line in raisePrinter.lines { - printer.write(line) + printer.write( + "default: throw new Error(\"Unknown \(enumName) tag returned from Swift: \" + String(tag));" + ) + } + printer.write("}") } - - printer.write( - "default: throw new Error(\"Unknown \(enumName) tag returned from Swift: \" + String(tag));" - ) - printer.unindent() - printer.write("}") - printer.unindent() printer.write("}") printer.unindent() printer.write("});") @@ -466,14 +461,14 @@ struct IntrinsicJSFragment: Sendable { printCode: { arguments, scope, printer, cleanup in let enumName = arguments[0] printer.write("const \(enumName) = {") - printer.indent() - for (index, enumCase) in enumDefinition.cases.enumerated() { - let caseName = enumCase.name.capitalizedFirstLetter - printer.write("\(caseName): \(index),") + printer.indent { + for (index, enumCase) in enumDefinition.cases.enumerated() { + let caseName = enumCase.name.capitalizedFirstLetter + printer.write("\(caseName): \(index),") + } } - printer.unindent() printer.write("};") - printer.write("") + printer.nextLine() return [] } @@ -486,17 +481,20 @@ struct IntrinsicJSFragment: Sendable { printCode: { arguments, scope, printer, cleanup in let enumName = arguments[0] printer.write("const \(enumName) = {") - printer.indent() - for enumCase in enumDefinition.cases { - let caseName = enumCase.name.capitalizedFirstLetter - let rawValue = enumCase.rawValue ?? enumCase.name - let formattedValue = SwiftEnumRawType.formatValue(rawValue, rawType: enumDefinition.rawType ?? "") + printer.indent { + for enumCase in enumDefinition.cases { + let caseName = enumCase.name.capitalizedFirstLetter + let rawValue = enumCase.rawValue ?? enumCase.name + let formattedValue = SwiftEnumRawType.formatValue( + rawValue, + rawType: enumDefinition.rawType ?? "" + ) - printer.write("\(caseName): \(formattedValue),") + printer.write("\(caseName): \(formattedValue),") + } } - printer.unindent() printer.write("};") - printer.write("") + printer.nextLine() return [] }