diff --git a/Sources/BinaryParsing/Parsers/FloatingPoint.swift b/Sources/BinaryParsing/Parsers/FloatingPoint.swift new file mode 100644 index 0000000..a947cce --- /dev/null +++ b/Sources/BinaryParsing/Parsers/FloatingPoint.swift @@ -0,0 +1,212 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Binary Parsing open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +extension Float16 { + /// Creates a `Float16` by parsing a big-endian value from the start of the + /// given parser span. + /// + /// - Parameter input: The `ParserSpan` to parse from. If parsing succeeds, + /// the start position of `input` is moved forward by 2 bytes. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// a `Float16`. + @discardableResult + public init(parsingBigEndian input: inout ParserSpan) throws(ParsingError) { + let bitPattern = try UInt16(parsingBigEndian: &input) + self = Self(bitPattern: bitPattern) + } + + /// Creates a `Float16` by parsing a little-endian value from the start of the + /// given parser span. + /// + /// - Parameter input: The `ParserSpan` to parse from. If parsing succeeds, + /// the start position of `input` is moved forward by 2 bytes. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// a `Float16`. + public init(parsingLittleEndian input: inout ParserSpan) throws(ParsingError) + { + let bitPattern = try UInt16(parsingLittleEndian: &input) + self = Self(bitPattern: bitPattern) + } + + /// Creates a `Float16` by parsing a value with the specified endianness from + /// the start of the given parser span. + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. If parsing succeeds, the start + /// position of `input` is moved forward by 2 bytes. + /// - endianness: The endianness to use when interpreting the parsed value. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// a `Float16`. + public init(parsing input: inout ParserSpan, endianness: Endianness) + throws(ParsingError) + { + let bitPattern = try UInt16(parsing: &input, endianness: endianness) + self = Self(bitPattern: bitPattern) + } +} + +extension Float { + /// Creates a `Float` by parsing a big-endian value from the start of the + /// given parser span. + /// + /// - Parameter input: The `ParserSpan` to parse from. If parsing succeeds, + /// the start position of `input` is moved forward by 4 bytes. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// a `Float`. + public init(parsingBigEndian input: inout ParserSpan) throws(ParsingError) { + let bitPattern = try UInt32(parsingBigEndian: &input) + self = Self(bitPattern: bitPattern) + } + + /// Creates a `Float` by parsing a little-endian value from the start of the + /// given parser span. + /// + /// - Parameter input: The `ParserSpan` to parse from. If parsing succeeds, + /// the start position of `input` is moved forward by 4 bytes. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// a `Float`. + public init(parsingLittleEndian input: inout ParserSpan) throws(ParsingError) + { + let bitPattern = try UInt32(parsingLittleEndian: &input) + self = Self(bitPattern: bitPattern) + } + + /// Creates a `Float` by parsing a value with the specified endianness from + /// the start of the given parser span. + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. If parsing succeeds, the start + /// position of `input` is moved forward by 4 bytes. + /// - endianness: The endianness to use when interpreting the parsed value. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// a `Float`. + public init(parsing input: inout ParserSpan, endianness: Endianness) + throws(ParsingError) + { + let bitPattern = try UInt32(parsing: &input, endianness: endianness) + self = Self(bitPattern: bitPattern) + } +} + +extension Double { + /// Creates a `Double` by parsing a big-endian value from the start of the + /// given parser span. + /// + /// - Parameter input: The `ParserSpan` to parse from. If parsing succeeds, + /// the start position of `input` is moved forward by 8 bytes. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// a `Double`. + public init(parsingBigEndian input: inout ParserSpan) throws(ParsingError) { + let bitPattern = try UInt64(parsingBigEndian: &input) + self = Self(bitPattern: bitPattern) + } + + /// Creates a `Double` by parsing a little-endian value from the start of the + /// given parser span. + /// + /// - Parameter input: The `ParserSpan` to parse from. If parsing succeeds, + /// the start position of `input` is moved forward by 8 bytes. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// a `Double`. + public init(parsingLittleEndian input: inout ParserSpan) throws(ParsingError) + { + let bitPattern = try UInt64(parsingLittleEndian: &input) + self = Self(bitPattern: bitPattern) + } + + /// Creates a `Double` by parsing a value with the specified endianness from + /// the start of the given parser span. + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. If parsing succeeds, the start + /// position of `input` is moved forward by 8 bytes. + /// - endianness: The endianness to use when interpreting the parsed value. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// a `Double`. + public init(parsing input: inout ParserSpan, endianness: Endianness) + throws(ParsingError) + { + let bitPattern = try UInt64(parsing: &input, endianness: endianness) + self = Self(bitPattern: bitPattern) + } +} + +#if !(os(Windows) || os(Android) || ($Embedded && !os(Linux) && !(os(macOS) || os(iOS) || os(watchOS) || os(tvOS)))) && (arch(i386) || arch(x86_64)) + +// Local development on a non-supported platform is super fun... +// +//#else +//public struct Float80: Sendable, Equatable { +// init(sign: FloatingPointSign, exponentBitPattern: UInt, significandBitPattern: UInt64) {} +//} + +extension Float80 { + private init(exponentAndSign: UInt16, significandBitPattern: UInt64) { + let sign: FloatingPointSign = exponentAndSign >> 15 == 0 ? .plus : .minus + self = Self( + sign: sign, + exponentBitPattern: UInt(exponentAndSign & 0x7FFF), + significandBitPattern: significandBitPattern) + } + + /// Creates a `Float80` by parsing a big-endian value from the start of the + /// given parser span. + /// + /// - Parameter input: The `ParserSpan` to parse from. If parsing succeeds, + /// the start position of `input` is moved forward by 10 bytes. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// a `Float80`. + public init(parsingBigEndian input: inout ParserSpan) throws(ParsingError) { + try input._checkCount(minimum: 10) + self = unsafe Float80( + exponentAndSign: UInt16(_unchecked: (), _parsingBigEndian: &input), + significandBitPattern: UInt64(_unchecked: (), _parsingBigEndian: &input)) + } + + /// Creates a `Float80` by parsing a little-endian value from the start of the + /// given parser span. + /// + /// - Parameter input: The `ParserSpan` to parse from. If parsing succeeds, + /// the start position of `input` is moved forward by 10 bytes. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// a `Float80`. + public init(parsingLittleEndian input: inout ParserSpan) throws(ParsingError) + { + // In little-endian format, significand comes first, then exponent and sign + try input._checkCount(minimum: 10) + let significandBitPattern = UInt64( + _unchecked: (), _parsingLittleEndian: &input) + self = unsafe Float80( + exponentAndSign: UInt16(_unchecked: (), _parsingLittleEndian: &input), + significandBitPattern: significandBitPattern) + } + + /// Creates a `Float80` by parsing a value with the specified endianness from + /// the start of the given parser span. + /// + /// - Parameters: + /// - input: The `ParserSpan` to parse from. If parsing succeeds, the start + /// position of `input` is moved forward by 10 bytes. + /// - endianness: The endianness to use when interpreting the parsed value. + /// - Throws: A `ParsingError` if `input` does not have enough bytes to store + /// a `Float80`. + public init(parsing input: inout ParserSpan, endianness: Endianness) + throws(ParsingError) + { + if endianness.isBigEndian { + try self.init(parsingBigEndian: &input) + } else { + try self.init(parsingLittleEndian: &input) + } + } +} + +#endif diff --git a/Tests/BinaryParsingTests/FloatingPointTests.swift b/Tests/BinaryParsingTests/FloatingPointTests.swift new file mode 100644 index 0000000..c31ede0 --- /dev/null +++ b/Tests/BinaryParsingTests/FloatingPointTests.swift @@ -0,0 +1,266 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Binary Parsing open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import BinaryParsing +import Testing + +enum Interesting { + static let float16s: [Float16] = [ + 0.0, 1.0, 1000, + .ulpOfOne, .leastNonzeroMagnitude, .leastNormalMagnitude, + .greatestFiniteMagnitude, .infinity, + .nan, .signalingNaN, + ] + + static let floats: [Float] = [ + 0.0, 1.0, 1000, + .ulpOfOne, .leastNonzeroMagnitude, .leastNormalMagnitude, + .greatestFiniteMagnitude, .infinity, + .nan, .signalingNaN, + ] + + static let doubles: [Double] = [ + 0.0, 1.0, 1000, + .ulpOfOne, .leastNonzeroMagnitude, .leastNormalMagnitude, + .greatestFiniteMagnitude, .infinity, + .nan, .signalingNaN, + ] + + static let float80OneLE: [UInt8] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 0x3f, + ] + + static let float80OneBE: [UInt8] = [ + 0x3f, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ] + + #if !(os(Windows) || os(Android) || ($Embedded && !os(Linux) && !(os(macOS) || os(iOS) || os(watchOS) || os(tvOS)))) && (arch(i386) || arch(x86_64)) + static let float80s: [Float80] = [ + 0.0, 1.0, 1000, + .ulpOfOne, .leastNonzeroMagnitude, .leastNormalMagnitude, + .greatestFiniteMagnitude, .infinity, + .nan, .signalingNaN, + ] + #endif +} + +struct FloatingPointTests { + @Test(arguments: Interesting.float16s) + func testFloat16RoundTrip(_ value: Float16) throws { + let bytesLE = Array(littleEndian: value.bitPattern) + let bytesBE = Array(bigEndian: value.bitPattern) + + do { + let value1 = try bytesLE.withParserSpan( + Float16.init(parsingLittleEndian:)) + let value2 = try bytesLE.withParserSpan { input in + try Float16(parsing: &input, endianness: .little) + } + + if value.isNaN { + #expect(value1.isNaN) + #expect(value2.isNaN) + if value.isSignalingNaN { + #expect(value1.isSignalingNaN) + #expect(value2.isSignalingNaN) + } + } else { + #expect(value1 == value) + #expect(value2 == value) + } + } + + do { + let value1 = try bytesBE.withParserSpan(Float16.init(parsingBigEndian:)) + let value2 = try bytesBE.withParserSpan { input in + try Float16(parsing: &input, endianness: .big) + } + + if value.isNaN { + #expect(value1.isNaN) + #expect(value2.isNaN) + if value.isSignalingNaN { + #expect(value1.isSignalingNaN) + #expect(value2.isSignalingNaN) + } + } else { + #expect(value1 == value) + #expect(value2 == value) + } + } + + // Check negative version of all values as well. + if value.sign == .plus { + try testFloat16RoundTrip(-value) + } + } + + @Test(arguments: Interesting.floats) + func testFloatRoundTrip(_ value: Float) throws { + let bytesLE = Array(littleEndian: value.bitPattern) + let bytesBE = Array(bigEndian: value.bitPattern) + + do { + let value1 = try bytesLE.withParserSpan(Float.init(parsingLittleEndian:)) + let value2 = try bytesLE.withParserSpan { input in + try Float(parsing: &input, endianness: .little) + } + + if value.isNaN { + #expect(value1.isNaN) + #expect(value2.isNaN) + if value.isSignalingNaN { + #expect(value1.isSignalingNaN) + #expect(value2.isSignalingNaN) + } + } else { + #expect(value1 == value) + #expect(value2 == value) + } + } + + do { + let value1 = try bytesBE.withParserSpan(Float.init(parsingBigEndian:)) + let value2 = try bytesBE.withParserSpan { input in + try Float(parsing: &input, endianness: .big) + } + + if value.isNaN { + #expect(value1.isNaN) + #expect(value2.isNaN) + if value.isSignalingNaN { + #expect(value1.isSignalingNaN) + #expect(value2.isSignalingNaN) + } + } else { + #expect(value1 == value) + #expect(value2 == value) + } + } + + // Check negative version of all values as well. + if value.sign == .plus { + try testFloatRoundTrip(-value) + } + } + + @Test(arguments: Interesting.doubles) + func testDoubleRoundTrip(_ value: Double) throws { + let bytesLE = Array(littleEndian: value.bitPattern) + let bytesBE = Array(bigEndian: value.bitPattern) + + do { + let value1 = try bytesLE.withParserSpan(Double.init(parsingLittleEndian:)) + let value2 = try bytesLE.withParserSpan { input in + try Double(parsing: &input, endianness: .little) + } + + if value.isNaN { + #expect(value1.isNaN) + #expect(value2.isNaN) + if value.isSignalingNaN { + #expect(value1.isSignalingNaN) + #expect(value2.isSignalingNaN) + } + } else { + #expect(value1 == value) + #expect(value2 == value) + } + } + + do { + let value1 = try bytesBE.withParserSpan(Double.init(parsingBigEndian:)) + let value2 = try bytesBE.withParserSpan { input in + try Double(parsing: &input, endianness: .big) + } + + if value.isNaN { + #expect(value1.isNaN) + #expect(value2.isNaN) + if value.isSignalingNaN { + #expect(value1.isSignalingNaN) + #expect(value2.isSignalingNaN) + } + } else { + #expect(value1 == value, "\(1)") + #expect(value2 == value) + } + } + + // Check negative version of all values as well. + if value.sign == .plus { + try testDoubleRoundTrip(-value) + } + } + + #if !(os(Windows) || os(Android) || ($Embedded && !os(Linux) && !(os(macOS) || os(iOS) || os(watchOS) || os(tvOS)))) && (arch(i386) || arch(x86_64)) + @Test + func staticFloat80() throws { + let leValue = try Interesting.float80OneLE.withParserSpan( + Float80.init(parsingLittleEndian:)) + let beValue = try Interesting.float80OneBE.withParserSpan( + Float80.init(parsingBigEndian:)) + + #expect(leValue == 1.0) + #expect(beValue == 1.0) + } + + @Test(arguments: Interesting.float80s) + func testFloat80RoundTrip(_ value: Float80) throws { + let bytesLE = Array(littleEndian: value) + let bytesBE = Array(bigEndian: value) + + do { + let value1 = try bytesLE.withParserSpan( + Float80.init(parsingLittleEndian:)) + let value2 = try bytesLE.withParserSpan { input in + try Float80(parsing: &input, endianness: .little) + } + + if value.isNaN { + #expect(value1.isNaN) + #expect(value2.isNaN) + if value.isSignalingNaN { + #expect(value1.isSignalingNaN) + #expect(value2.isSignalingNaN) + } + } else { + #expect(value1 == value) + #expect(value2 == value) + } + } + + do { + let value1 = try bytesBE.withParserSpan(Float80.init(parsingBigEndian:)) + let value2 = try bytesBE.withParserSpan { input in + try Float80(parsing: &input, endianness: .big) + } + + if value.isNaN { + #expect(value1.isNaN) + #expect(value2.isNaN) + if value.isSignalingNaN { + #expect(value1.isSignalingNaN) + #expect(value2.isSignalingNaN) + } + } else { + #expect(value1 == value) + #expect(value2 == value) + } + } + + // Check negative version of all values as well. + if value.sign == .plus { + try testFloat80RoundTrip(-value) + } + } + #endif +} diff --git a/Tests/BinaryParsingTests/TestingSupport.swift b/Tests/BinaryParsingTests/TestingSupport.swift index 7a3d551..956e343 100644 --- a/Tests/BinaryParsingTests/TestingSupport.swift +++ b/Tests/BinaryParsingTests/TestingSupport.swift @@ -137,6 +137,35 @@ extension Array where Element == UInt8 { } self = out } + + #if !(os(Windows) || os(Android) || ($Embedded && !os(Linux) && !(os(macOS) || os(iOS) || os(watchOS) || os(tvOS)))) && (arch(i386) || arch(x86_64)) + init(littleEndian value: Float80) { + self = [] + Swift.withUnsafeBytes(of: value.significandBitPattern.littleEndian) { + bytes in + self.append(contentsOf: bytes) + } + let signAndExponent = UInt16( + truncatingIfNeeded: + value.exponentBitPattern | ((value.sign == .minus ? 1 : 0) << 15)) + Swift.withUnsafeBytes(of: signAndExponent.littleEndian) { bytes in + self.append(contentsOf: bytes) + } + } + + init(bigEndian value: Float80) { + self = [] + let signAndExponent = UInt16( + truncatingIfNeeded: + value.exponentBitPattern | ((value.sign == .minus ? 1 : 0) << 15)) + Swift.withUnsafeBytes(of: signAndExponent.bigEndian) { bytes in + self.append(contentsOf: bytes) + } + Swift.withUnsafeBytes(of: value.significandBitPattern.bigEndian) { bytes in + self.append(contentsOf: bytes) + } + } + #endif } /// A seeded random number generator type.