diff --git a/Sources/NewCodable/CommonDecodableAdoption.swift b/Sources/NewCodable/CommonDecodableAdoption.swift index 4f5a2916e..04a0f25a7 100644 --- a/Sources/NewCodable/CommonDecodableAdoption.swift +++ b/Sources/NewCodable/CommonDecodableAdoption.swift @@ -66,6 +66,14 @@ extension Int64: CommonDecodable { } } +extension Int128: CommonDecodable { + @_alwaysEmitIntoClient + @inline(__always) + public static func decode(from decoder: inout D) throws(CodingError.Decoding) -> Self { + return try decoder.decode(Self.self) + } +} + extension UInt: CommonDecodable { @_alwaysEmitIntoClient @inline(__always) @@ -106,6 +114,14 @@ extension UInt64: CommonDecodable { } } +extension UInt128: CommonDecodable { + @_alwaysEmitIntoClient + @inline(__always) + public static func decode(from decoder: inout D) throws(CodingError.Decoding) -> Self { + return try decoder.decode(Self.self) + } +} + extension Float: CommonDecodable { @_alwaysEmitIntoClient @inline(__always) diff --git a/Sources/NewCodable/CommonDecodableProtocols.swift b/Sources/NewCodable/CommonDecodableProtocols.swift index 2630c441f..0caac5587 100644 --- a/Sources/NewCodable/CommonDecodableProtocols.swift +++ b/Sources/NewCodable/CommonDecodableProtocols.swift @@ -123,6 +123,9 @@ public protocol CommonDecoder: ~Escapable { @_lifetime(self: copy self) mutating func decode(_ hint: Int64.Type) throws(CodingError.Decoding) -> Int64 + @_lifetime(self: copy self) + mutating func decode(_ hint: Int128.Type) throws(CodingError.Decoding) -> Int128 + @_lifetime(self: copy self) mutating func decode(_ hint: UInt.Type) throws(CodingError.Decoding) -> UInt @@ -138,6 +141,9 @@ public protocol CommonDecoder: ~Escapable { @_lifetime(self: copy self) mutating func decode(_ hint: UInt64.Type) throws(CodingError.Decoding) -> UInt64 + @_lifetime(self: copy self) + mutating func decode(_ hint: UInt128.Type) throws(CodingError.Decoding) -> UInt128 + @_lifetime(self: copy self) mutating func decode(_ hint: Float.Type) throws(CodingError.Decoding) -> Float @@ -393,6 +399,9 @@ public extension CommonDecoder where Self: ~Escapable { @_lifetime(self: copy self) mutating func decode(_ hint: Int64.Type) throws(CodingError.Decoding) -> Int64 { throw CodingError.unsupportedDecodingType("Int64") } + @_lifetime(self: copy self) + mutating func decode(_ hint: Int128.Type) throws(CodingError.Decoding) -> Int128 { throw CodingError.unsupportedDecodingType("Int128") } + @_lifetime(self: copy self) mutating func decode(_ hint: UInt.Type) throws(CodingError.Decoding) -> UInt { throw CodingError.unsupportedDecodingType("UInt") } @@ -408,6 +417,9 @@ public extension CommonDecoder where Self: ~Escapable { @_lifetime(self: copy self) mutating func decode(_ hint: UInt64.Type) throws(CodingError.Decoding) -> UInt64 { throw CodingError.unsupportedDecodingType("UInt64") } + @_lifetime(self: copy self) + mutating func decode(_ hint: UInt128.Type) throws(CodingError.Decoding) -> UInt128 { throw CodingError.unsupportedDecodingType("UInt128") } + @_lifetime(self: copy self) mutating func decode(_ hint: Float.Type) throws(CodingError.Decoding) -> Float { throw CodingError.unsupportedDecodingType("Float") } diff --git a/Sources/NewCodable/CommonEncodableAdoption.swift b/Sources/NewCodable/CommonEncodableAdoption.swift index f08b5dab2..f42ac1287 100644 --- a/Sources/NewCodable/CommonEncodableAdoption.swift +++ b/Sources/NewCodable/CommonEncodableAdoption.swift @@ -55,6 +55,12 @@ extension Int64: CommonEncodable { } } +extension Int128: CommonEncodable { + public func encode(to encoder: inout some CommonEncoder & ~Copyable & ~Escapable) throws(CodingError.Encoding) { + try encoder.encode(self) + } +} + extension UInt: CommonEncodable { public func encode(to encoder: inout some CommonEncoder & ~Copyable & ~Escapable) throws(CodingError.Encoding) { try encoder.encode(self) @@ -85,6 +91,12 @@ extension UInt64: CommonEncodable { } } +extension UInt128: CommonEncodable { + public func encode(to encoder: inout some CommonEncoder & ~Copyable & ~Escapable) throws(CodingError.Encoding) { + try encoder.encode(self) + } +} + extension Float: CommonEncodable { public func encode(to encoder: inout some CommonEncoder & ~Copyable & ~Escapable) throws(CodingError.Encoding) { try encoder.encode(self) diff --git a/Sources/NewCodable/JSON/JSONDecoderProtocols.swift b/Sources/NewCodable/JSON/JSONDecoderProtocols.swift index 06db9b0e9..0de0639b1 100644 --- a/Sources/NewCodable/JSON/JSONDecoderProtocols.swift +++ b/Sources/NewCodable/JSON/JSONDecoderProtocols.swift @@ -209,6 +209,9 @@ public protocol JSONDecoderProtocol: ~Escapable { @_lifetime(self: copy self) mutating func decode(_ hint: Int64.Type) throws(CodingError.Decoding) -> Int64 + @_lifetime(self: copy self) + mutating func decode(_ hint: Int128.Type) throws(CodingError.Decoding) -> Int128 + @_lifetime(self: copy self) mutating func decode(_ hint: UInt.Type) throws(CodingError.Decoding) -> UInt @@ -224,6 +227,9 @@ public protocol JSONDecoderProtocol: ~Escapable { @_lifetime(self: copy self) mutating func decode(_ hint: UInt64.Type) throws(CodingError.Decoding) -> UInt64 + @_lifetime(self: copy self) + mutating func decode(_ hint: UInt128.Type) throws(CodingError.Decoding) -> UInt128 + @_lifetime(self: copy self) mutating func decode(_ hint: Float.Type) throws(CodingError.Decoding) -> Float @@ -387,6 +393,14 @@ extension Int64: JSONDecodable { } } +extension Int128: JSONDecodable { + @inline(__always) + @_alwaysEmitIntoClient + public static func decode(from decoder: inout D) throws(CodingError.Decoding) -> Self { + return try decoder.decode(Self.self) + } +} + extension UInt: JSONDecodable { @inline(__always) @_alwaysEmitIntoClient @@ -427,6 +441,14 @@ extension UInt64: JSONDecodable { } } +extension UInt128: JSONDecodable { + @inline(__always) + @_alwaysEmitIntoClient + public static func decode(from decoder: inout D) throws(CodingError.Decoding) -> Self { + return try decoder.decode(Self.self) + } +} + extension Float: JSONDecodable { @inline(__always) @_alwaysEmitIntoClient diff --git a/Sources/NewCodable/JSON/JSONEncoderProtocols.swift b/Sources/NewCodable/JSON/JSONEncoderProtocols.swift index f5e3af8db..e6d698f60 100644 --- a/Sources/NewCodable/JSON/JSONEncoderProtocols.swift +++ b/Sources/NewCodable/JSON/JSONEncoderProtocols.swift @@ -98,6 +98,13 @@ extension Int64: JSONEncodable { } } +extension Int128: JSONEncodable { + @inlinable + public func encode(to encoder: inout JSONDirectEncoder) throws(CodingError.Encoding) { + try encoder.encode(self) + } +} + extension UInt: JSONEncodable { @inlinable public func encode(to encoder: inout JSONDirectEncoder) throws(CodingError.Encoding) { @@ -133,6 +140,13 @@ extension UInt64: JSONEncodable { } } +extension UInt128: JSONEncodable { + @inlinable + public func encode(to encoder: inout JSONDirectEncoder) throws(CodingError.Encoding) { + try encoder.encode(self) + } +} + extension Float: JSONEncodable { @inlinable public func encode(to encoder: inout JSONDirectEncoder) throws(CodingError.Encoding) { diff --git a/Sources/NewCodable/JSON/JSONParserDecoder.swift b/Sources/NewCodable/JSON/JSONParserDecoder.swift index 81b075fea..2f4112281 100644 --- a/Sources/NewCodable/JSON/JSONParserDecoder.swift +++ b/Sources/NewCodable/JSON/JSONParserDecoder.swift @@ -1018,6 +1018,17 @@ extension JSONParserDecoder { return try state.decode(hint) } + @_lifetime(self: copy self) + public mutating func decode(_ hint: Int128.Type) throws(CodingError.Decoding) -> Int128 { + do { + try state.reader.consumeWhitespaceAndPeek() + } catch { + throw error.at(self.codingPath) + } + return try state.decode(hint) + } + + @_lifetime(self: copy self) public mutating func decode(_ hint: UInt.Type) throws(CodingError.Decoding) -> UInt { do { @@ -1067,6 +1078,16 @@ extension JSONParserDecoder { } return try state.decode(hint) } + + @_lifetime(self: copy self) + public mutating func decode(_ hint: UInt128.Type) throws(CodingError.Decoding) -> UInt128 { + do { + try state.reader.consumeWhitespaceAndPeek() + } catch { + throw error.at(self.codingPath) + } + return try state.decode(hint) + } @_lifetime(self: copy self) public mutating func decode(_ hint: Float.Type) throws(CodingError.Decoding) -> Float { diff --git a/Sources/NewCodable/JSON/JSONPrimitive.swift b/Sources/NewCodable/JSON/JSONPrimitive.swift index ec86e739a..7cd97b73c 100644 --- a/Sources/NewCodable/JSON/JSONPrimitive.swift +++ b/Sources/NewCodable/JSON/JSONPrimitive.swift @@ -255,11 +255,10 @@ extension JSONPrimitive { return try number[type] } func decode(_ type: Int128.Type) throws -> Int128 { - fatalError("TODO") -// guard case .number(let number) = self else { -// fatalError("ERROR") -// } -// return try number[type] + guard case .number(let number) = self else { + fatalError("ERROR") + } + return try number[type] } func decode(_ type: UInt.Type) throws -> UInt { guard case .number(let number) = self else { @@ -305,7 +304,12 @@ extension JSONPrimitive { } return try number[type] } - func decode(_ type: UInt128.Type) throws -> UInt128 { fatalError("TODO") } + func decode(_ type: UInt128.Type) throws -> UInt128 { + guard case .number(let number) = self else { + fatalError("ERROR") + } + return try number[type] + } func decode(_ type: T.Type) throws -> T where T : Decodable { let decoder = _JSONValueDecoder(self) return try type.init(from: decoder) diff --git a/Sources/NewCodable/JSON/ParserState.swift b/Sources/NewCodable/JSON/ParserState.swift index 692f711df..6ef93fdc7 100644 --- a/Sources/NewCodable/JSON/ParserState.swift +++ b/Sources/NewCodable/JSON/ParserState.swift @@ -96,11 +96,13 @@ extension JSONParserDecoder { @_lifetime(self: copy self) mutating func decode(_ t: Int16.Type) throws(CodingError.Decoding) -> Int16 { try decode() } @_lifetime(self: copy self) mutating func decode(_ t: Int32.Type) throws(CodingError.Decoding) -> Int32 { try decode() } @_lifetime(self: copy self) mutating func decode(_ t: Int64.Type) throws(CodingError.Decoding) -> Int64 { try decode() } + @_lifetime(self: copy self) mutating func decode(_ t: Int128.Type) throws(CodingError.Decoding) -> Int128 { try decode() } @_lifetime(self: copy self) mutating func decode(_ t: UInt.Type) throws(CodingError.Decoding) -> UInt { try decode() } @_lifetime(self: copy self) mutating func decode(_ t: UInt8.Type) throws(CodingError.Decoding) -> UInt8 { try decode() } @_lifetime(self: copy self) mutating func decode(_ t: UInt16.Type) throws(CodingError.Decoding) -> UInt16 { try decode() } @_lifetime(self: copy self) mutating func decode(_ t: UInt32.Type) throws(CodingError.Decoding) -> UInt32 { try decode() } @_lifetime(self: copy self) mutating func decode(_ t: UInt64.Type) throws(CodingError.Decoding) -> UInt64 { try decode() } + @_lifetime(self: copy self) mutating func decode(_ t: UInt128.Type) throws(CodingError.Decoding) -> UInt128 { try decode() } @usableFromInline internal struct FloatingPointNonConformingStringValueVisitor: DecodingStringVisitor { @@ -173,7 +175,6 @@ extension JSONParserDecoder { @inlinable @inline(__always) mutating func decode() throws(CodingError.Decoding) -> T { - // TODO: TEST NEGATIVE FLOATS HERE. I think `parseInteger` consumes the `-` and doesn't restore it on returning .retryAsFloatingPoint do throws(_JSONDecodingError) { switch try reader.parseInteger(as: T.self) ^^ .jsonError { case .pureInteger(let integer): @@ -186,7 +187,13 @@ extension JSONParserDecoder { throw .json(JSONError.numberIsNotRepresentableInSwift(parsed: String(double))) } - // TODO: Classic JSONDecoder would retry Decimal -> integer parsing + // Double only has 53 bits of significand, so values with magnitude >= 2^53 + // may have been rounded. Reject them to avoid silently returning wrong integers. + // TODO: Classic JSONDecoder would retry Decimal -> integer parsing for these values. + if double.magnitude >= Double(sign: .plus, exponent: Double.significandBitCount + 1, significand: 1) { + throw .json(JSONError.numberIsNotRepresentableInSwift(parsed: String(double))) + } + return integer case .notANumber: throw .decoding(decodingError(expectedTypeDescription: "integer number")) @@ -1198,7 +1205,13 @@ extension JSONParserDecoder { switch peek() { case ._minus: moveReaderIndex(forwardBy: 1) - return try _parseIntegerDigits(isNegative: true) + let result = try _parseIntegerDigits(isNegative: true) as IntegerParseResult + if case .retryAsFloatingPoint = result { + // _parseIntegerDigits rewinds to its own start, but the '-' was consumed + // before it was called. Rewind one more byte so parseFloatingPoint sees it. + moveReaderIndex(forwardBy: -1) + } + return result case ._plus: moveReaderIndex(forwardBy: 1) fallthrough diff --git a/Tests/NewCodableTests/JSONEncodingDecodingTests.swift b/Tests/NewCodableTests/JSONEncodingDecodingTests.swift index ae2fe27d5..08a96fdd3 100644 --- a/Tests/NewCodableTests/JSONEncodingDecodingTests.swift +++ b/Tests/NewCodableTests/JSONEncodingDecodingTests.swift @@ -933,19 +933,18 @@ struct JSONEncodingDecodingTests { // MARK: - Type coercion @Test func typeCoercion() { - // TODO: (U)Int128 support. _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int].self) _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int8].self) _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int16].self) _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int32].self) _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int64].self) - // _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int128].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int128].self) _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt].self) _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt8].self) _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt16].self) _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt32].self) _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt64].self) - // _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt128].self) + _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt128].self) _testRoundTripTypeCoercionFailure(of: [false, true], as: [Float].self) _testRoundTripTypeCoercionFailure(of: [false, true], as: [Double].self) _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int], as: [Bool].self) @@ -953,13 +952,13 @@ struct JSONEncodingDecodingTests { _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int16], as: [Bool].self) _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int32], as: [Bool].self) _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int64], as: [Bool].self) - // _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int128], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int128], as: [Bool].self) _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt], as: [Bool].self) _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt8], as: [Bool].self) _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt16], as: [Bool].self) _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt32], as: [Bool].self) _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt64], as: [Bool].self) - // _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt128], as: [Bool].self) + _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt128], as: [Bool].self) _testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Float], as: [Bool].self) _testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Double], as: [Bool].self) } @@ -1522,84 +1521,83 @@ struct JSONEncodingDecodingTests { _testRoundTrip(of: testValue) } - // TODO: (U)Int128 support + proting. - // @Test func roundTrippingInt128() { - // for i128 in [Int128.min, - // Int128.min + 1, - // -0x1_0000_0000_0000_0000, - // 0x0_8000_0000_0000_0000, - // -1, - // 0, - // 0x7fff_ffff_ffff_ffff, - // 0x8000_0000_0000_0000, - // 0xffff_ffff_ffff_ffff, - // 0x1_0000_0000_0000_0000, - // .max] { - // _testRoundTrip(of: i128) - // } - // } - // - // @Test func int128SlowPath() throws { - // let decoder = JSONDecoder() - // let work: [Int128] = [18446744073709551615, -18446744073709551615] - // for value in work { - // // force the slow-path by appending ".0" - // let json = "\(value).0".data(using: .utf8)! - // #expect(try value == decoder.decode(Int128.self, from: json)) - // } - // // These should work, but making them do so probably requires - // // rewriting the slow path to use a dedicated parser. For now, - // // we ensure that they throw instead of returning some bogus - // // result. - // let shouldWorkButDontYet: [Int128] = [ - // .min, -18446744073709551616, 18446744073709551616, .max - // ] - // for value in shouldWorkButDontYet { - // // force the slow-path by appending ".0" - // let json = "\(value).0".data(using: .utf8)! - // #expect(throws: (any Error).self) { - // try decoder.decode(Int128.self, from: json) - // } - // } - // } - // - // @Test func roundTrippingUInt128() { - // for u128 in [UInt128.zero, - // 1, - // 0x0000_0000_0000_0000_7fff_ffff_ffff_ffff, - // 0x0000_0000_0000_0000_8000_0000_0000_0000, - // 0x0000_0000_0000_0000_ffff_ffff_ffff_ffff, - // 0x0000_0000_0000_0001_0000_0000_0000_0000, - // 0x7fff_ffff_ffff_ffff_ffff_ffff_ffff_ffff, - // 0x8000_0000_0000_0000_0000_0000_0000_0000, - // .max] { - // _testRoundTrip(of: u128) - // } - // } - // - // @Test func uint128SlowPath() throws { - // let decoder = JSONDecoder() - // let work: [UInt128] = [18446744073709551615] - // for value in work { - // // force the slow-path by appending ".0" - // let json = "\(value).0".data(using: .utf8)! - // #expect(try value == decoder.decode(UInt128.self, from: json)) - // } - // // These should work, but making them do so probably requires - // // rewriting the slow path to use a dedicated parser. For now, - // // we ensure that they throw instead of returning some bogus - // // result. - // let shouldWorkButDontYet: [UInt128] = [ - // 18446744073709551616, .max - // ] - // for value in shouldWorkButDontYet { - // // force the slow-path by appending ".0" - // let json = "\(value).0".data(using: .utf8)! - // #expect(throws: (any Error).self) { - // try decoder.decode(UInt128.self, from: json) - // } - // } - // } + @Test func roundTrippingInt128() { + for i128 in [Int128.min, + Int128.min + 1, + -0x1_0000_0000_0000_0000, + 0x0_8000_0000_0000_0000, + -1, + 0, + 0x7fff_ffff_ffff_ffff, + 0x8000_0000_0000_0000, + 0xffff_ffff_ffff_ffff, + 0x1_0000_0000_0000_0000, + .max] { + _testRoundTrip(of: i128) + } + } + + @Test func int128SlowPath() throws { + let decoder = NewJSONDecoder() + let work: [Int128] = [0, 1, -1, 256, -256, 65536, -65536] + for value in work { + // force the slow-path by appending ".0" + let json = "\(value).0".data(using: .utf8)! + #expect(try value == decoder.decode(Int128.self, from: json)) + } + // These should work, but making them do so probably requires + // rewriting the slow path to use a dedicated parser. For now, + // we ensure that they throw instead of returning some bogus + // result. + let shouldWorkButDontYet: [Int128] = [ + 18446744073709551615, -18446744073709551615, // These values work in JSONDecoder because of its Decimal fallback. + .min, -18446744073709551616, 18446744073709551616, .max + ] + for value in shouldWorkButDontYet { + let json = "\(value).0".data(using: .utf8)! + #expect(throws: (any Error).self) { + try decoder.decode(Int128.self, from: json) + } + } + } + + @Test func roundTrippingUInt128() { + for u128 in [UInt128.zero, + 1, + 0x0000_0000_0000_0000_7fff_ffff_ffff_ffff, + 0x0000_0000_0000_0000_8000_0000_0000_0000, + 0x0000_0000_0000_0000_ffff_ffff_ffff_ffff, + 0x0000_0000_0000_0001_0000_0000_0000_0000, + 0x7fff_ffff_ffff_ffff_ffff_ffff_ffff_ffff, + 0x8000_0000_0000_0000_0000_0000_0000_0000, + .max] { + _testRoundTrip(of: u128) + } + } + + @Test func uint128SlowPath() throws { + let decoder = NewJSONDecoder() + let work: [UInt128] = [0, 1, 256, 65536] + for value in work { + // force the slow-path by appending ".0" + let json = "\(value).0".data(using: .utf8)! + #expect(try value == decoder.decode(UInt128.self, from: json)) + } + // These should work, but making them do so probably requires + // rewriting the slow path to use a dedicated parser. For now, + // we ensure that they throw instead of returning some bogus + // result. + let shouldWorkButDontYet: [UInt128] = [ + 18446744073709551615, // This value works in JSONDecoder because of its Decimal fallback. + 18446744073709551616, .max + ] + for value in shouldWorkButDontYet { + let json = "\(value).0".data(using: .utf8)! + #expect(throws: (any Error).self) { + try decoder.decode(UInt128.self, from: json) + } + } + } @Test func roundTrippingDoubleValues() { struct Numbers : JSONEncodable, JSONDecodable, Equatable {