Skip to content

Commit 4d1dfb4

Browse files
[6.0] Int128 support in json (#791)
* Bare minimum [U]Int128 support for JSON encode/decode This doesn't yet tackle _slowpath_unwrapFixedWidthInteger, but at least allows us to round-trip [U]Int128 with our own encoder and decoder * Switch to if #available check for Int128 tests Also added some slow-path testing for [U]Int128, to ensure that we throw an error as expected in these cases instead of returning an incorrect value.
1 parent ebd0f42 commit 4d1dfb4

File tree

3 files changed

+153
-0
lines changed

3 files changed

+153
-0
lines changed

Sources/FoundationEssentials/JSON/JSONDecoder.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,11 @@ extension JSONDecoderImpl : SingleValueDecodingContainer {
11281128
func decode(_: Int64.Type) throws -> Int64 {
11291129
try decodeFixedWidthInteger()
11301130
}
1131+
1132+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
1133+
func decode(_: Int128.Type) throws -> Int128 {
1134+
try decodeFixedWidthInteger()
1135+
}
11311136

11321137
func decode(_: UInt.Type) throws -> UInt {
11331138
try decodeFixedWidthInteger()
@@ -1148,6 +1153,11 @@ extension JSONDecoderImpl : SingleValueDecodingContainer {
11481153
func decode(_: UInt64.Type) throws -> UInt64 {
11491154
try decodeFixedWidthInteger()
11501155
}
1156+
1157+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
1158+
func decode(_: UInt128.Type) throws -> UInt128 {
1159+
try decodeFixedWidthInteger()
1160+
}
11511161

11521162
func decode<T: Decodable>(_ type: T.Type) throws -> T {
11531163
try self.unwrap(self.topValue, as: type, for: codingPathNode, _CodingKey?.none)
@@ -1274,6 +1284,11 @@ extension JSONDecoderImpl {
12741284
func decode(_: Int64.Type, forKey key: K) throws -> Int64 {
12751285
try decodeFixedWidthInteger(key: key)
12761286
}
1287+
1288+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
1289+
func decode(_: Int128.Type, forKey key: K) throws -> Int128 {
1290+
try decodeFixedWidthInteger(key: key)
1291+
}
12771292

12781293
func decode(_: UInt.Type, forKey key: K) throws -> UInt {
12791294
try decodeFixedWidthInteger(key: key)
@@ -1294,6 +1309,11 @@ extension JSONDecoderImpl {
12941309
func decode(_: UInt64.Type, forKey key: K) throws -> UInt64 {
12951310
try decodeFixedWidthInteger(key: key)
12961311
}
1312+
1313+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
1314+
func decode(_: UInt128.Type, forKey key: K) throws -> UInt128 {
1315+
try decodeFixedWidthInteger(key: key)
1316+
}
12971317

12981318
func decode<T: Decodable>(_ type: T.Type, forKey key: K) throws -> T {
12991319
try self.impl.unwrap(try getValue(forKey: key), as: type, for: codingPathNode, key)
@@ -1456,6 +1476,11 @@ extension JSONDecoderImpl {
14561476
mutating func decode(_: Int64.Type) throws -> Int64 {
14571477
try decodeFixedWidthInteger()
14581478
}
1479+
1480+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
1481+
mutating func decode(_: Int128.Type) throws -> Int128 {
1482+
try decodeFixedWidthInteger()
1483+
}
14591484

14601485
mutating func decode(_: UInt.Type) throws -> UInt {
14611486
try decodeFixedWidthInteger()
@@ -1476,6 +1501,11 @@ extension JSONDecoderImpl {
14761501
mutating func decode(_: UInt64.Type) throws -> UInt64 {
14771502
try decodeFixedWidthInteger()
14781503
}
1504+
1505+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
1506+
mutating func decode(_: UInt128.Type) throws -> UInt128 {
1507+
try decodeFixedWidthInteger()
1508+
}
14791509

14801510
mutating func decode<T: Decodable>(_ type: T.Type) throws -> T {
14811511
let value = try self.peekNextValue(ofType: type)

Sources/FoundationEssentials/JSON/JSONEncoder.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,10 @@ private struct _JSONKeyedEncodingContainer<K : CodingKey> : KeyedEncodingContain
712712
public mutating func encode(_ value: Int64, forKey key: Key) throws {
713713
reference.insert(self.encoder.wrap(value), for: _converted(key))
714714
}
715+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
716+
public mutating func encode(_ value: Int128, forKey key: Key) throws {
717+
reference.insert(self.encoder.wrap(value), for: _converted(key))
718+
}
715719
public mutating func encode(_ value: UInt, forKey key: Key) throws {
716720
reference.insert(self.encoder.wrap(value), for: _converted(key))
717721
}
@@ -727,6 +731,10 @@ private struct _JSONKeyedEncodingContainer<K : CodingKey> : KeyedEncodingContain
727731
public mutating func encode(_ value: UInt64, forKey key: Key) throws {
728732
reference.insert(self.encoder.wrap(value), for: _converted(key))
729733
}
734+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
735+
public mutating func encode(_ value: UInt128, forKey key: Key) throws {
736+
reference.insert(self.encoder.wrap(value), for: _converted(key))
737+
}
730738
public mutating func encode(_ value: String, forKey key: Key) throws {
731739
reference.insert(self.encoder.wrap(value), for: _converted(key))
732740
}
@@ -827,11 +835,15 @@ private struct _JSONUnkeyedEncodingContainer : UnkeyedEncodingContainer {
827835
public mutating func encode(_ value: Int16) throws { self.reference.insert(self.encoder.wrap(value)) }
828836
public mutating func encode(_ value: Int32) throws { self.reference.insert(self.encoder.wrap(value)) }
829837
public mutating func encode(_ value: Int64) throws { self.reference.insert(self.encoder.wrap(value)) }
838+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
839+
public mutating func encode(_ value: Int128) throws { self.reference.insert(self.encoder.wrap(value)) }
830840
public mutating func encode(_ value: UInt) throws { self.reference.insert(self.encoder.wrap(value)) }
831841
public mutating func encode(_ value: UInt8) throws { self.reference.insert(self.encoder.wrap(value)) }
832842
public mutating func encode(_ value: UInt16) throws { self.reference.insert(self.encoder.wrap(value)) }
833843
public mutating func encode(_ value: UInt32) throws { self.reference.insert(self.encoder.wrap(value)) }
834844
public mutating func encode(_ value: UInt64) throws { self.reference.insert(self.encoder.wrap(value)) }
845+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
846+
public mutating func encode(_ value: UInt128) throws { self.reference.insert(self.encoder.wrap(value)) }
835847
public mutating func encode(_ value: String) throws { self.reference.insert(self.encoder.wrap(value)) }
836848

837849
public mutating func encode(_ value: Float) throws {
@@ -908,6 +920,12 @@ extension __JSONEncoder : SingleValueEncodingContainer {
908920
assertCanEncodeNewValue()
909921
self.storage.push(ref: wrap(value))
910922
}
923+
924+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
925+
public func encode(_ value: Int128) throws {
926+
assertCanEncodeNewValue()
927+
self.storage.push(ref: wrap(value))
928+
}
911929

912930
public func encode(_ value: UInt) throws {
913931
assertCanEncodeNewValue()
@@ -933,6 +951,12 @@ extension __JSONEncoder : SingleValueEncodingContainer {
933951
assertCanEncodeNewValue()
934952
self.storage.push(ref: wrap(value))
935953
}
954+
955+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
956+
public func encode(_ value: UInt128) throws {
957+
assertCanEncodeNewValue()
958+
self.storage.push(ref: wrap(value))
959+
}
936960

937961
public func encode(_ value: String) throws {
938962
assertCanEncodeNewValue()
@@ -967,11 +991,15 @@ private extension __JSONEncoder {
967991
@inline(__always) func wrap(_ value: Int16) -> JSONReference { .number(from: value) }
968992
@inline(__always) func wrap(_ value: Int32) -> JSONReference { .number(from: value) }
969993
@inline(__always) func wrap(_ value: Int64) -> JSONReference { .number(from: value) }
994+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
995+
@inline(__always) func wrap(_ value: Int128) -> JSONReference { .number(from: value) }
970996
@inline(__always) func wrap(_ value: UInt) -> JSONReference { .number(from: value) }
971997
@inline(__always) func wrap(_ value: UInt8) -> JSONReference { .number(from: value) }
972998
@inline(__always) func wrap(_ value: UInt16) -> JSONReference { .number(from: value) }
973999
@inline(__always) func wrap(_ value: UInt32) -> JSONReference { .number(from: value) }
9741000
@inline(__always) func wrap(_ value: UInt64) -> JSONReference { .number(from: value) }
1001+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
1002+
@inline(__always) func wrap(_ value: UInt128) -> JSONReference { .number(from: value) }
9751003
@inline(__always) func wrap(_ value: String) -> JSONReference { .string(value) }
9761004

9771005
@inline(__always)
@@ -1302,11 +1330,15 @@ extension Int8 : _JSONSimpleValueArrayElement { }
13021330
extension Int16 : _JSONSimpleValueArrayElement { }
13031331
extension Int32 : _JSONSimpleValueArrayElement { }
13041332
extension Int64 : _JSONSimpleValueArrayElement { }
1333+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
1334+
extension Int128 : _JSONSimpleValueArrayElement { }
13051335
extension UInt : _JSONSimpleValueArrayElement { }
13061336
extension UInt8 : _JSONSimpleValueArrayElement { }
13071337
extension UInt16 : _JSONSimpleValueArrayElement { }
13081338
extension UInt32 : _JSONSimpleValueArrayElement { }
13091339
extension UInt64 : _JSONSimpleValueArrayElement { }
1340+
@available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
1341+
extension UInt128 : _JSONSimpleValueArrayElement { }
13101342
extension String: _JSONSimpleValueArrayElement {
13111343
fileprivate func jsonRepresentation(options: JSONEncoder._Options) -> String {
13121344
self.serializedForJSON(withoutEscapingSlashes: options.outputFormatting.contains(.withoutEscapingSlashes))

Tests/FoundationEssentialsTests/JSONEncoderTests.swift

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,9 @@ final class JSONEncoderTests : XCTestCase {
612612
_testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt16], as: [Bool].self)
613613
_testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt32], as: [Bool].self)
614614
_testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt64], as: [Bool].self)
615+
if #available(macOS 15.0, *) {
616+
_testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt128], as: [Bool].self)
617+
}
615618
_testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Float], as: [Bool].self)
616619
_testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Double], as: [Bool].self)
617620
}
@@ -1323,6 +1326,94 @@ final class JSONEncoderTests : XCTestCase {
13231326
let testValue = Numbers(floats: [.greatestFiniteMagnitude, .leastNormalMagnitude], doubles: [.greatestFiniteMagnitude, .leastNormalMagnitude])
13241327
_testRoundTrip(of: testValue)
13251328
}
1329+
1330+
func test_roundTrippingInt128() {
1331+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
1332+
let values = [
1333+
Int128.min,
1334+
Int128.min + 1,
1335+
-0x1_0000_0000_0000_0000,
1336+
0x0_8000_0000_0000_0000,
1337+
-1,
1338+
0,
1339+
0x7fff_ffff_ffff_ffff,
1340+
0x8000_0000_0000_0000,
1341+
0xffff_ffff_ffff_ffff,
1342+
0x1_0000_0000_0000_0000,
1343+
.max
1344+
]
1345+
for i128 in values {
1346+
_testRoundTrip(of: i128)
1347+
}
1348+
}
1349+
}
1350+
1351+
func test_Int128SlowPath() {
1352+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
1353+
let decoder = JSONDecoder()
1354+
let work: [Int128] = [18446744073709551615, -18446744073709551615]
1355+
for value in work {
1356+
// force the slow-path by appending ".0"
1357+
let json = "\(value).0".data(using: String._Encoding.utf8)!
1358+
XCTAssertEqual(value, try? decoder.decode(Int128.self, from: json))
1359+
}
1360+
// These should work, but making them do so probably requires
1361+
// rewriting the slow path to use a dedicated parser. For now,
1362+
// we ensure that they throw instead of returning some bogus
1363+
// result.
1364+
let shouldWorkButDontYet: [Int128] = [
1365+
.min, -18446744073709551616, 18446744073709551616, .max
1366+
]
1367+
for value in shouldWorkButDontYet {
1368+
// force the slow-path by appending ".0"
1369+
let json = "\(value).0".data(using: String._Encoding.utf8)!
1370+
XCTAssertThrowsError(try decoder.decode(Int128.self, from: json))
1371+
}
1372+
}
1373+
}
1374+
1375+
func test_roundTrippingUInt128() {
1376+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
1377+
let values = [
1378+
UInt128.zero,
1379+
1,
1380+
0x0000_0000_0000_0000_7fff_ffff_ffff_ffff,
1381+
0x0000_0000_0000_0000_8000_0000_0000_0000,
1382+
0x0000_0000_0000_0000_ffff_ffff_ffff_ffff,
1383+
0x0000_0000_0000_0001_0000_0000_0000_0000,
1384+
0x7fff_ffff_ffff_ffff_ffff_ffff_ffff_ffff,
1385+
0x8000_0000_0000_0000_0000_0000_0000_0000,
1386+
.max
1387+
]
1388+
for u128 in values {
1389+
_testRoundTrip(of: u128)
1390+
}
1391+
}
1392+
}
1393+
1394+
func test_UInt128SlowPath() {
1395+
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
1396+
let decoder = JSONDecoder()
1397+
let work: [UInt128] = [18446744073709551615]
1398+
for value in work {
1399+
// force the slow-path by appending ".0"
1400+
let json = "\(value).0".data(using: String._Encoding.utf8)!
1401+
XCTAssertEqual(value, try? decoder.decode(UInt128.self, from: json))
1402+
}
1403+
// These should work, but making them do so probably requires
1404+
// rewriting the slow path to use a dedicated parser. For now,
1405+
// we ensure that they throw instead of returning some bogus
1406+
// result.
1407+
let shouldWorkButDontYet: [UInt128] = [
1408+
18446744073709551616, .max
1409+
]
1410+
for value in shouldWorkButDontYet {
1411+
// force the slow-path by appending ".0"
1412+
let json = "\(value).0".data(using: String._Encoding.utf8)!
1413+
XCTAssertThrowsError(try decoder.decode(UInt128.self, from: json))
1414+
}
1415+
}
1416+
}
13261417

13271418
func test_roundTrippingDoubleValues() {
13281419
struct Numbers : Codable, Equatable {

0 commit comments

Comments
 (0)