From cf6fd8f7e7dc11fc29fbdabd5a01e6b73e04367a Mon Sep 17 00:00:00 2001 From: Coen ten Thije Boonkkamp Date: Sun, 27 Jul 2025 11:33:14 +0200 Subject: [PATCH] Add support for throwing functions in .memberwise conversions - Add @_disfavoredOverload memberwise overload for throwing initializers - Implement ThrowingMemberwise struct with proper error propagation - Add comprehensive tests for both throwing and non-throwing cases - Maintain backward compatibility and all existing safety checks --- Sources/Parsing/Conversions/Memberwise.swift | 55 +++++++++++++++++ Tests/ParsingTests/MemberwiseTests.swift | 65 ++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 Tests/ParsingTests/MemberwiseTests.swift diff --git a/Sources/Parsing/Conversions/Memberwise.swift b/Sources/Parsing/Conversions/Memberwise.swift index a1d1925950..8f485b0f06 100644 --- a/Sources/Parsing/Conversions/Memberwise.swift +++ b/Sources/Parsing/Conversions/Memberwise.swift @@ -121,6 +121,14 @@ extension Conversion { ) -> Self where Self == Conversions.Memberwise { .init(initializer: initializer) } + + @inlinable + @_disfavoredOverload + public static func memberwise( + _ initializer: @escaping (Values) throws -> Struct + ) -> Self where Self == Conversions.ThrowingMemberwise { + .init(initializer: initializer) + } } extension Conversions { @@ -170,4 +178,51 @@ extension Conversions { return unsafeBitCast(output, to: Values.self) } } + + public struct ThrowingMemberwise: Conversion { + @usableFromInline + let initializer: (Values) throws -> Struct + + @usableFromInline + init(initializer: @escaping (Values) throws -> Struct) { + self.initializer = initializer + } + + @inlinable + public func apply(_ input: Values) throws -> Struct { + try self.initializer(input) + } + + @inlinable + public func unapply(_ output: Struct) throws -> Values { + let ptr = unsafeBitCast(Struct.self as Any.Type, to: UnsafeRawPointer.self) + guard ptr.load(as: Int.self) == 512 + else { + throw ConvertingError( + """ + memberwise: Can't convert \(Values.self) to non-struct type \(Struct.self). This \ + conversion should only be used with a memberwise initializer matching the memory layout \ + of the struct. The "memberwise" initializer is the internal, compiler-generated \ + initializer that specifies its arguments in the same order as the struct specifies its \ + properties. + """ + ) + } + guard + MemoryLayout.alignment == MemoryLayout.alignment, + MemoryLayout.size == MemoryLayout.size + else { + throw ConvertingError( + """ + memberwise: Can't convert \(Values.self) type \(Struct.self) as their memory layouts \ + differ. This conversion should only be used with a memberwise initializer matching the \ + memory layout of the struct. The "memberwise" initializer is the internal, \ + compiler-generated initializer that specifies its arguments in the same order as the \ + struct specifies its properties. + """ + ) + } + return unsafeBitCast(output, to: Values.self) + } + } } diff --git a/Tests/ParsingTests/MemberwiseTests.swift b/Tests/ParsingTests/MemberwiseTests.swift new file mode 100644 index 0000000000..eed05b30d8 --- /dev/null +++ b/Tests/ParsingTests/MemberwiseTests.swift @@ -0,0 +1,65 @@ +import Parsing +import XCTest + +final class MemberwiseTests: XCTestCase { + func testNonThrowingMemberwise() throws { + struct Point { + let x: Double + let y: Double + } + + let parser = ParsePrint(.memberwise(Point.init(x:y:))) { + "(" + Double.parser() + "," + Double.parser() + ")" + } + + let result = try parser.parse("(1.5,-2.3)") + XCTAssertEqual(result.x, 1.5) + XCTAssertEqual(result.y, -2.3) + + let printed = try parser.print(Point(x: 3.0, y: 4.0)) + XCTAssertEqual(printed, "(3.0,4.0)") + } + + func testThrowingMemberwise() throws { + struct ValidatedPoint { + let x: Double + let y: Double + + init(x: Double, y: Double) throws { + guard x.isFinite && y.isFinite else { + throw ValidationError.invalidCoordinate + } + self.x = x + self.y = y + } + } + + enum ValidationError: Error { + case invalidCoordinate + } + + let parser = ParsePrint(.memberwise(ValidatedPoint.init(x:y:))) { + "(" + Double.parser() + "," + Double.parser() + ")" + } + + // Test successful parsing + let result = try parser.parse("(1.5,-2.3)") + XCTAssertEqual(result.x, 1.5) + XCTAssertEqual(result.y, -2.3) + + // Test successful printing + let printed = try parser.print(ValidatedPoint(x: 3.0, y: 4.0)) + XCTAssertEqual(printed, "(3.0,4.0)") + + // Test throwing during parsing - should propagate the ValidationError + XCTAssertThrowsError(try parser.parse("(inf,-2.3)")) + } +} \ No newline at end of file