Skip to content

[5.0] Make SIMD types Codable. #22110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion stdlib/public/core/SIMDVector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ prefix operator .!
/// conform to `SIMD`.
public protocol SIMDStorage {
/// The type of scalars in the vector space.
associatedtype Scalar : Hashable
associatedtype Scalar : Codable, Hashable

/// The number of scalars, or elements, in the vector.
var scalarCount: Int { get }
Expand Down Expand Up @@ -51,6 +51,7 @@ public protocol SIMDScalar {

/// A SIMD vector of a fixed number of elements.
public protocol SIMD : SIMDStorage,
Codable,
Hashable,
CustomStringConvertible,
ExpressibleByArrayLiteral {
Expand Down Expand Up @@ -85,6 +86,42 @@ public extension SIMD {
for i in indices { hasher.combine(self[i]) }
}

/// Encodes the scalars of this vector into the given encoder in an unkeyed
/// container.
///
/// This function throws an error if any values are invalid for the given
/// encoder's format.
///
/// - Parameter encoder: The encoder to write data to.
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
for i in indices {
try container.encode(self[i])
}
}

/// Creates a new vector by decoding scalars from the given decoder.
///
/// This initializer throws an error if reading from the decoder fails, or
/// if the data read is corrupted or otherwise invalid.
///
/// - Parameter decoder: The decoder to read data from.
init(from decoder: Decoder) throws {
self.init()
var container = try decoder.unkeyedContainer()
guard container.count == scalarCount else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Expected vector with exactly \(scalarCount) elements."
)
)
}
for i in indices {
self[i] = try container.decode(Scalar.self)
}
}

/// A textual description of the vector.
var description: String {
get {
Expand Down
4 changes: 4 additions & 0 deletions test/api-digester/Outputs/stability-stdlib-abi.swift.expected
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,7 @@ Var _RawSetStorage.empty has declared type change from _EmptySetSingleton to __E

Func tryReallocateUniquelyReferenced(buffer:newMinimumCapacity:) has been removed

Protocol SIMD has added inherited protocol Decodable
Protocol SIMD has added inherited protocol Encodable
Protocol SIMD has generic signature change from <τ_0_0 : CustomStringConvertible, τ_0_0 : ExpressibleByArrayLiteral, τ_0_0 : Hashable, τ_0_0 : SIMDStorage, τ_0_0.MaskStorage : SIMD, τ_0_0.MaskStorage.Scalar : FixedWidthInteger, τ_0_0.MaskStorage.Scalar : SignedInteger> to <τ_0_0 : CustomStringConvertible, τ_0_0 : Decodable, τ_0_0 : Encodable, τ_0_0 : ExpressibleByArrayLiteral, τ_0_0 : Hashable, τ_0_0 : SIMDStorage, τ_0_0.MaskStorage : SIMD, τ_0_0.MaskStorage.Scalar : FixedWidthInteger, τ_0_0.MaskStorage.Scalar : SignedInteger>
Protocol SIMDStorage has generic signature change from <τ_0_0.Scalar : Hashable> to <τ_0_0.Scalar : Decodable, τ_0_0.Scalar : Encodable, τ_0_0.Scalar : Hashable>
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
Func tryReallocateUniquelyReferenced(buffer:newMinimumCapacity:) has been removed
Protocol SIMD has added inherited protocol Decodable
Protocol SIMD has added inherited protocol Encodable
Protocol SIMD has generic signature change from <Self : CustomStringConvertible, Self : ExpressibleByArrayLiteral, Self : Hashable, Self : SIMDStorage, Self.MaskStorage : SIMD, Self.MaskStorage.Scalar : FixedWidthInteger, Self.MaskStorage.Scalar : SignedInteger> to <Self : CustomStringConvertible, Self : Decodable, Self : Encodable, Self : ExpressibleByArrayLiteral, Self : Hashable, Self : SIMDStorage, Self.MaskStorage : SIMD, Self.MaskStorage.Scalar : FixedWidthInteger, Self.MaskStorage.Scalar : SignedInteger>
Protocol SIMDStorage has generic signature change from <Self.Scalar : Hashable> to <Self.Scalar : Decodable, Self.Scalar : Encodable, Self.Scalar : Hashable>
91 changes: 91 additions & 0 deletions test/stdlib/SIMD.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// RUN: %target-run-simple-swift
// REQUIRES: executable_test
// REQUIRES: objc_interop

import Foundation
import StdlibUnittest

let SIMDCodableTests = TestSuite("SIMDCodable")

// Round an integer to the closest representable JS integer value
func jsInteger<T>(_ value: T) -> T
where T : SIMD, T.Scalar : FixedWidthInteger {
// Attempt to round-trip though Double; if that fails it's because the
// rounded value is too large to fit in T, so use the largest value that
// does fit instead.
let upperBound = T.Scalar(Double(T.Scalar.max).nextDown)
var result = T()
for i in result.indices {
result[i] = T.Scalar(exactly: Double(value[i])) ?? upperBound
}
return result
}

func testRoundTrip<T>(_ for: T.Type)
where T : SIMD, T.Scalar : FixedWidthInteger {
let input = jsInteger(T.random(in: T.Scalar.min ... T.Scalar.max))
let encoder = JSONEncoder()
let decoder = JSONDecoder()
do {
let data = try encoder.encode(input)
let output = try decoder.decode(T.self, from: data)
expectEqual(input, output)
}
catch {
expectUnreachableCatch(error)
}
}

func testRoundTrip<T>(_ for: T.Type)
where T : SIMD, T.Scalar : BinaryFloatingPoint,
T.Scalar.RawSignificand : FixedWidthInteger {
let input = T.random(in: -16 ..< 16)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
do {
let data = try encoder.encode(input)
let output = try decoder.decode(T.self, from: data)
expectEqual(input, output)
}
catch {
expectUnreachableCatch(error)
}
}

// Very basic round-trip test. We can be much more sophisticated in the future,
// but we want to at least exercise the API. Also need to add some negative
// tests for the error paths, and test a more substantial set of types.
SIMDCodableTests.test("roundTrip") {
testRoundTrip(SIMD2<Int8>.self)
testRoundTrip(SIMD3<Int8>.self)
testRoundTrip(SIMD4<Int8>.self)
testRoundTrip(SIMD2<UInt8>.self)
testRoundTrip(SIMD3<UInt8>.self)
testRoundTrip(SIMD4<UInt8>.self)
testRoundTrip(SIMD2<Int32>.self)
testRoundTrip(SIMD3<Int32>.self)
testRoundTrip(SIMD4<Int32>.self)
testRoundTrip(SIMD2<UInt32>.self)
testRoundTrip(SIMD3<UInt32>.self)
testRoundTrip(SIMD4<UInt32>.self)
testRoundTrip(SIMD2<Int>.self)
testRoundTrip(SIMD3<Int>.self)
testRoundTrip(SIMD4<Int>.self)
testRoundTrip(SIMD2<UInt>.self)
testRoundTrip(SIMD3<UInt>.self)
testRoundTrip(SIMD4<UInt>.self)
/* Apparently these fail to round trip not only for i386 but also on older
macOS versions, so we'll disable them entirely for now.
#if !arch(i386)
// https://bugs.swift.org/browse/SR-9759
testRoundTrip(SIMD2<Float>.self)
testRoundTrip(SIMD3<Float>.self)
testRoundTrip(SIMD4<Float>.self)
testRoundTrip(SIMD2<Double>.self)
testRoundTrip(SIMD3<Double>.self)
testRoundTrip(SIMD4<Double>.self)
#endif
*/
}

runAllTests()