From 47ef1731ba2c303b093140a320e43ab2f8311038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20Bystr=C3=B6m=20Ericsson?= Date: Tue, 29 Oct 2024 06:15:40 +0100 Subject: [PATCH] Rework of StdlibInt+Floats.swift tests (#108) (#110). --- Sources/TestKit2/Global+Types.swift | 5 + Sources/TestKit2/Protocols/SwiftIEEE754.swift | 29 ++ .../StdlibIntKitTests/StdlibInt+Floats.swift | 355 ++---------------- 3 files changed, 68 insertions(+), 321 deletions(-) create mode 100644 Sources/TestKit2/Protocols/SwiftIEEE754.swift diff --git a/Sources/TestKit2/Global+Types.swift b/Sources/TestKit2/Global+Types.swift index f8146be8..82a92fe0 100644 --- a/Sources/TestKit2/Global+Types.swift +++ b/Sources/TestKit2/Global+Types.swift @@ -38,6 +38,11 @@ public let typesAsCoreIntegersAsUnsigned: [any CoreIntegerAsUnsigned.Type] = [ // MARK: + Floats //=----------------------------------------------------------------------------= +public let typesAsSwiftIEEE754: [any SwiftIEEE754.Type] = [ + Float32.self, + Float64.self, +] + public let typesAsSwiftBinaryFloatingPoint: [any Swift.BinaryFloatingPoint.Type] = [ Float32.self, Float64.self, diff --git a/Sources/TestKit2/Protocols/SwiftIEEE754.swift b/Sources/TestKit2/Protocols/SwiftIEEE754.swift new file mode 100644 index 00000000..b8a49e2c --- /dev/null +++ b/Sources/TestKit2/Protocols/SwiftIEEE754.swift @@ -0,0 +1,29 @@ +//=----------------------------------------------------------------------------= +// This source file is part of the Ultimathnum open source project. +// +// Copyright (c) 2023 Oscar Byström Ericsson +// Licensed under Apache License, Version 2.0 +// +// See http://www.apache.org/licenses/LICENSE-2.0 for license information. +//=----------------------------------------------------------------------------= + +//*============================================================================* +// MARK: * Swift IEEE 754 +//*============================================================================* + +public protocol SwiftIEEE754: Swift.BinaryFloatingPoint { + + //=------------------------------------------------------------------------= + // MARK: Initializers + //=------------------------------------------------------------------------= + + static func random(in range: Range, using randomness: inout some Swift.RandomNumberGenerator) -> Self + static func random(in range: ClosedRange, using randomness: inout some Swift.RandomNumberGenerator) -> Self +} + +//*============================================================================* +// MARK: * Swift IEEE 754 x Models +//*============================================================================* + +extension Float32: SwiftIEEE754 { } +extension Float64: SwiftIEEE754 { } diff --git a/Tests/StdlibIntKitTests/StdlibInt+Floats.swift b/Tests/StdlibIntKitTests/StdlibInt+Floats.swift index abc58e92..15fdacd1 100644 --- a/Tests/StdlibIntKitTests/StdlibInt+Floats.swift +++ b/Tests/StdlibIntKitTests/StdlibInt+Floats.swift @@ -28,345 +28,58 @@ import TestKit2 /// - TODO: Test `StdlibInt` forwarding in generic `BinaryInteger` tests. /// @Suite struct StdlibIntTestsOnFloats { - - //=------------------------------------------------------------------------= - // MARK: Tests x Swift.BinaryFloatingPoint - //=------------------------------------------------------------------------= - - @Test( - "StdlibInt ← Swift.BinaryFloatingPoint - nan is nil", - arguments: typesAsSwiftBinaryFloatingPoint - ) func initSwiftBinaryFloatingPointNanIsNil(type: any Swift.BinaryFloatingPoint.Type) { - whereIs(type) - func whereIs(_ type: T.Type) where T: Swift.BinaryFloatingPoint { - Ɣexpect( T.nan, is: nil) - Ɣexpect(-T.nan, is: nil) - } - } - - @Test( - "StdlibInt ← Swift.BinaryFloatingPoint - infinity is nil", - arguments: typesAsSwiftBinaryFloatingPoint - ) func initSwiftBinaryFloatingPointInfinityIsNil(type: any Swift.BinaryFloatingPoint.Type) { - whereIs(type) - - func whereIs(_ type: T.Type) where T: Swift.BinaryFloatingPoint { - Ɣexpect( T.infinity, is: nil) - Ɣexpect(-T.infinity, is: nil) - } - } - - @Test( - "StdlibInt ← Swift.BinaryFloatingPoint - rounds towards zero", - arguments: typesAsSwiftBinaryFloatingPoint - ) func initSwiftBinaryFloatingPointRoundsTowardsZero(type: any Swift.BinaryFloatingPoint.Type) { - whereIs(type) - - func whereIs(_ type: T.Type) where T: Swift.BinaryFloatingPoint { - Ɣexpect( 2.00 as T, is: 2 as StdlibInt, exactly: true) - Ɣexpect( 1.75 as T, is: 1 as StdlibInt) - Ɣexpect( 1.50 as T, is: 1 as StdlibInt) - Ɣexpect( 1.25 as T, is: 1 as StdlibInt) - Ɣexpect( 1.00 as T, is: 1 as StdlibInt, exactly: true) - Ɣexpect( 0.75 as T, is: 0 as StdlibInt) - Ɣexpect( 0.50 as T, is: 0 as StdlibInt) - Ɣexpect( 0.25 as T, is: 0 as StdlibInt) - Ɣexpect( 0.00 as T, is: 0 as StdlibInt, exactly: true) - Ɣexpect( 0.25 as T, is: 0 as StdlibInt) - Ɣexpect( 0.50 as T, is: 0 as StdlibInt) - Ɣexpect( 0.75 as T, is: 0 as StdlibInt) - Ɣexpect(-1.00 as T, is: -1 as StdlibInt, exactly: true) - Ɣexpect(-1.25 as T, is: -1 as StdlibInt) - Ɣexpect(-1.50 as T, is: -1 as StdlibInt) - Ɣexpect(-1.75 as T, is: -1 as StdlibInt) - Ɣexpect(-2.00 as T, is: -2 as StdlibInt, exactly: true) - - Ɣexpect( T.pi, is: 3 as StdlibInt) - Ɣexpect(-T.pi, is: -3 as StdlibInt) - - Ɣexpect( T.ulpOfOne, is: 0 as StdlibInt) - Ɣexpect(-T.ulpOfOne, is: 0 as StdlibInt) - - Ɣexpect( T.leastNormalMagnitude, is: 0 as StdlibInt) - Ɣexpect(-T.leastNormalMagnitude, is: 0 as StdlibInt) - - Ɣexpect( T.leastNonzeroMagnitude, is: 0 as StdlibInt) - Ɣexpect(-T.leastNonzeroMagnitude, is: 0 as StdlibInt) - } - } - - @Test("StdlibInt ← Swift.BinaryFloatingPoint - greatest finite magnitude [32-bit]") - func initGreatestFiniteMagnitudeAsFloat32() { - let positive = IXL(340282346638528859811704183484516925440) - - Ɣexpect( Float32.greatestFiniteMagnitude, is: StdlibInt( positive), exactly: true) - Ɣexpect(-Float32.greatestFiniteMagnitude, is: StdlibInt(-positive), exactly: true) - } - - @Test("StdlibInt ← Swift.BinaryFloatingPoint - greatest finite magnitude [64-bit]") - func initGreatestFiniteMagnitudeAsFloat64() { - let positive = IXL(""" - 0000000000017976931348623157081452742373170435679807056752584499\ - 6598917476803157260780028538760589558632766878171540458953514382\ - 4642343213268894641827684675467035375169860499105765512820762454\ - 9009038932894407586850845513394230458323690322294816580855933212\ - 3348274797826204144723168738177180919299881250404026184124858368 - """)! - - Ɣexpect( Float64.greatestFiniteMagnitude, is: StdlibInt( positive), exactly: true) - Ɣexpect(-Float64.greatestFiniteMagnitude, is: StdlibInt(-positive), exactly: true) - } - //=------------------------------------------------------------------------= // MARK: Tests //=------------------------------------------------------------------------= - /// Checks some bit patterns for exponents >= 52 (64-bit). - /// - /// 1000000000000000000000000000000000000000000000000000110011000011 → - /// 1100000000000000000000000000000000000000000000000000110011000011 → - /// 1110000000000000000000000000000000000000000000000000110011000011 → - /// 1111000000000000000000000000000000000000000000000000110011000011 → - /// - /// 1111111111111111111111111111111111111111111111111000110011000011 → - /// 1111111111111111111111111111111111111111111111111100110011000011 → - /// 1111111111111111111111111111111111111111111111111110110011000011 → - /// 1111111111111111111111111111111111111111111111111111110011000011 → - /// @Test( - "StdlibInt ← Swift.BinaryFloatingPoint - [large][negative]", - arguments: typesAsSwiftBinaryFloatingPoint - ) func initLargeNegativeFloats(source: any Swift.BinaryFloatingPoint.Type) { - whereIs(source: source, exponents: 32, steps: source.significandBitCount) - - func whereIs(source: T.Type, exponents: Int, steps: Int) where T: Swift.BinaryFloatingPoint { - //=----------------------------------= - let start = T.significandBitCount - //=----------------------------------= - for exponent in start ..< start + exponents { - var source = T(sign: .minus, exponent: T.Exponent(exponent), significand: 1) - var sourceStep: T = source.ulp - var destination = StdlibInt.isSigned ? StdlibInt(-1) << exponent : nil - var destinationStep = StdlibInt(exactly: sourceStep)! - - for _ in 0 ..< steps { - source -= sourceStep - sourceStep += sourceStep - destination? -= destinationStep - destinationStep += destinationStep - Ɣexpect(source, is: destination, exactly: true) - } - } + "StdlibInt/floats: from SwiftIEEE754 vs StdlibInt.Base", + Tag.List.tags(.forwarding, .generic, .random), + arguments: fuzzers + ) func forwarding(randomness: consuming FuzzerInt) throws { + for source in typesAsSwiftIEEE754 { + try whereIs(source: source) } - } - - /// Checks some bit patterns for exponents >= 52 (64-bit). - /// - /// 0000000000000000000000000000000000000000000000000000110011000011 → - /// 1000000000000000000000000000000000000000000000000000110011000011 → - /// 0100000000000000000000000000000000000000000000000000110011000011 → - /// 1100000000000000000000000000000000000000000000000000110011000011 → - /// - /// 0010000000000000000000000000000000000000000000000000110011000011 → - /// 1010000000000000000000000000000000000000000000000000110011000011 → - /// 0110000000000000000000000000000000000000000000000000110011000011 → - /// 1110000000000000000000000000000000000000000000000000110011000011 → - /// - @Test( - "StdlibInt ← Swift.BinaryFloatingPoint - [large][negative][min]", - arguments: typesAsSwiftBinaryFloatingPoint - ) func initLargeNegativeFloatsNearMinSignificandBitPattern(source: any Swift.BinaryFloatingPoint.Type) { - whereIs(source: source, exponents: 32, steps: 32) - - func whereIs(source: T.Type, exponents: Int, steps: Int) where T: Swift.BinaryFloatingPoint { - //=----------------------------------= - let start = T.significandBitCount - //=----------------------------------= - for exponent in start ..< start + exponents { - var source = T(sign: .minus, exponent: T.Exponent(exponent), significand: 1) - let sourceStep: T = source.ulp - var destination = StdlibInt.isSigned ? StdlibInt(-1) << exponent : nil - let destinationStep = StdlibInt(exactly: sourceStep)! - - for _ in 0 ..< steps { - Ɣexpect(source, is: destination, exactly: true) - source -= sourceStep - destination? -= destinationStep - } - } - } - } - - /// Checks some bit patterns for exponents >= 52 (64-bit). - /// - /// 1111111111111111111111111111111111111111111111111111110011000011 → - /// 0111111111111111111111111111111111111111111111111111110011000011 → - /// 1011111111111111111111111111111111111111111111111111110011000011 → - /// 0011111111111111111111111111111111111111111111111111110011000011 → - /// - /// 1101111111111111111111111111111111111111111111111111110011000011 → - /// 0101111111111111111111111111111111111111111111111111110011000011 → - /// 1001111111111111111111111111111111111111111111111111110011000011 → - /// 0001111111111111111111111111111111111111111111111111110011000011 → - /// - @Test( - "StdlibInt ← Swift.BinaryFloatingPoint - [large][negative][max]", - arguments: typesAsSwiftBinaryFloatingPoint - ) func initLargeNegativeFloatsNearMaxSignificandBitPattern(source: any Swift.BinaryFloatingPoint.Type) { - whereIs(source: source, exponents: 32, steps: 32) - func whereIs(source: T.Type, exponents: Int, steps: Int) where T: Swift.BinaryFloatingPoint { - //=----------------------------------= - let start = T.significandBitCount + 1 - //=----------------------------------= - for exponent in start ..< start + exponents { - var source = T(sign: .minus, exponent: T.Exponent(exponent), significand: 1) - let sourceStep: T = source.nextUp.ulp - var destination = StdlibInt.isSigned ? StdlibInt(-1) << exponent : nil - let destinationStep = StdlibInt(exactly: sourceStep)! - - for _ in 0 ..< steps { - source += sourceStep - destination? += destinationStep - Ɣexpect(source, is: destination, exactly: true) - } - } - } - } - - /// Checks some bit patterns for exponents >= 52 (64-bit). - /// - /// 1000000000000000000000000000000000000000000000000000110011000010 → - /// 1100000000000000000000000000000000000000000000000000110011000010 → - /// 1110000000000000000000000000000000000000000000000000110011000010 → - /// 1111000000000000000000000000000000000000000000000000110011000010 → - /// - /// 1111111111111111111111111111111111111111111111111000110011000010 → - /// 1111111111111111111111111111111111111111111111111100110011000010 → - /// 1111111111111111111111111111111111111111111111111110110011000010 → - /// 1111111111111111111111111111111111111111111111111111110011000010 → - /// - @Test( - "StdlibInt ← Swift.BinaryFloatingPoint - [large][positive]", - arguments: typesAsSwiftBinaryFloatingPoint - ) func initLargePositiveFloats(source: any Swift.BinaryFloatingPoint.Type) { - whereIs(source: source, exponents: 32, steps: source.significandBitCount) - - func whereIs(source: T.Type, exponents: Int, steps: Int) where T: Swift.BinaryFloatingPoint { - //=----------------------------------= - let start = T.significandBitCount - //=----------------------------------= - for exponent in start ..< start + exponents { - var source = T(sign: .plus, exponent: T.Exponent(exponent), significand: 1) - var sourceStep: T = source.ulp - var destination = StdlibInt(1) << exponent - var destinationStep = StdlibInt(exactly: sourceStep)! - - for _ in 0 ..< steps { - source += sourceStep - sourceStep += sourceStep - destination += destinationStep - destinationStep += destinationStep - Ɣexpect(source, is: destination, exactly: true) + func whereIs(source: A.Type) throws where A: SwiftIEEE754 { + let limits: [A] = [1.0, 1024.0, A.greatestFiniteMagnitude/2] + for bounds: ClosedRange in limits.lazy.map({ -$0...$0 }) { + for _ in 0 ..< 64 { + let float = A.random(in: bounds, using: &randomness.stdlib) + let rounded = float.rounded(FloatingPointRoundingRule.towardZero) + let integer = try #require(IXL.leniently(float)?.map(StdlibInt.init)) + + try #require(StdlibInt( (float)) == integer.value) + try #require(StdlibInt(exactly: (float)) == integer.optional()) + try #require(StdlibInt( rounded) == integer.value) + try #require(StdlibInt(exactly: rounded) == integer.value) } } } } - /// Checks some bit patterns for exponents >= 52 (64-bit). - /// - /// 0000000000000000000000000000000000000000000000000000110011000010 → - /// 1000000000000000000000000000000000000000000000000000110011000010 → - /// 0100000000000000000000000000000000000000000000000000110011000010 → - /// 1100000000000000000000000000000000000000000000000000110011000010 → - /// - /// 0010000000000000000000000000000000000000000000000000110011000010 → - /// 1010000000000000000000000000000000000000000000000000110011000010 → - /// 0110000000000000000000000000000000000000000000000000110011000010 → - /// 1110000000000000000000000000000000000000000000000000110011000010 → - /// @Test( - "StdlibInt ← Swift.BinaryFloatingPoint - [large][positive][min]", - arguments: typesAsSwiftBinaryFloatingPoint - ) func initLargePositiveFloatsNearMinSignificandBitPattern(source: any Swift.BinaryFloatingPoint.Type) { - whereIs(source: source, exponents: 32, steps: 32) - - func whereIs(source: T.Type, exponents: Int, steps: Int) where T: Swift.BinaryFloatingPoint { - //=----------------------------------= - let start = T.significandBitCount - //=----------------------------------= - for exponent in start ..< start + exponents { - var source = T(sign: .plus, exponent: T.Exponent(exponent), significand: 1) - let sourceStep: T = source.ulp - var destination = StdlibInt(1) << exponent - let destinationStep = StdlibInt(exactly: sourceStep)! - - for _ in 0 ..< steps { - Ɣexpect(source, is: destination, exactly: true) - source += sourceStep - destination += destinationStep - } - } + "StdlibInt/floats: round-tripping SwiftIEEE754 integers", + Tag.List.tags(.forwarding, .generic, .random), + arguments: fuzzers + ) func roundtripping(randomness: consuming FuzzerInt) throws { + for source in typesAsSwiftIEEE754 { + try whereIs(source: source) } - } - - /// Checks some bit patterns for exponents >= 52 (64-bit). - /// - /// 1111111111111111111111111111111111111111111111111111110011000010 → - /// 0111111111111111111111111111111111111111111111111111110011000010 → - /// 1011111111111111111111111111111111111111111111111111110011000010 → - /// 0011111111111111111111111111111111111111111111111111110011000010 → - /// - /// 1101111111111111111111111111111111111111111111111111110011000010 → - /// 0101111111111111111111111111111111111111111111111111110011000010 → - /// 1001111111111111111111111111111111111111111111111111110011000010 → - /// 0001111111111111111111111111111111111111111111111111110011000010 → - /// - @Test( - "StdlibInt ← Swift.BinaryFloatingPoint - [large][positive][max]", - arguments: typesAsSwiftBinaryFloatingPoint - ) func initLargePositiveFloatsNearMaxSignificandBitPattern(source: any Swift.BinaryFloatingPoint.Type) { - whereIs(source: source, exponents: 32, steps: 32) - func whereIs(source: T.Type, exponents: Int, steps: Int) where T: Swift.BinaryFloatingPoint { - //=----------------------------------= - let start = T.significandBitCount + 1 - //=----------------------------------= - for exponent in start ..< start + exponents { - var source = T(sign: .plus, exponent: T.Exponent(exponent), significand: 1) - let sourceStep: T = source.nextDown.ulp - var destination = StdlibInt(1) << exponent - let destinationStep = StdlibInt(exactly: sourceStep)! - - for _ in 0 ..< steps { - source -= sourceStep - destination -= destinationStep - Ɣexpect(source, is: destination, exactly: true) + func whereIs(source: A.Type) throws where A: SwiftIEEE754 { + let limits: [A] = [1.0, 1024.0, A.greatestFiniteMagnitude/2] + for bounds: ClosedRange in limits.lazy.map({ -$0...$0 }) { + for _ in 0 ..< 64 { + let float = A.random(in: bounds, using: &randomness.stdlib) + let rounded = float.rounded(FloatingPointRoundingRule.towardZero) + let integer = try #require(StdlibInt(exactly: rounded)) + + try #require(rounded == A.init (integer), "integer → float") + try #require(integer == StdlibInt(float), "integer ← float") } } } } - - //=------------------------------------------------------------------------= - // MARK: Utilities - //=------------------------------------------------------------------------= - - func Ɣexpect( - _ source: Source, - is destination: StdlibInt?, - exactly: Bool = false, - at location: SourceLocation = #_sourceLocation - ) where Source: Swift.BinaryFloatingPoint { - - if let destination { - #expect(StdlibInt(source) == destination) - } - - if let destination, exactly { - #expect(Source(destination) == source) - } - - #expect(StdlibInt(exactly: source) == (exactly ? destination : nil)) - } }