From 8c4fa3f9356ad0cbac07d25634813143c7144473 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 24 Apr 2025 20:57:09 +0200 Subject: [PATCH 1/5] Proposal to generate `UUID`s using `RandomNumberGenerator`s This PR adds a proposal to generate `UUID's` using `RandomNumberGenerator`s --- Proposals/NNNN-random-uuid.md | 57 +++++++++++++++++++ Sources/FoundationEssentials/UUID.swift | 44 ++++++++++++++ .../FoundationEssentialsTests/UUIDTests.swift | 19 +++++++ 3 files changed, 120 insertions(+) create mode 100644 Proposals/NNNN-random-uuid.md diff --git a/Proposals/NNNN-random-uuid.md b/Proposals/NNNN-random-uuid.md new file mode 100644 index 000000000..4a17eb8d7 --- /dev/null +++ b/Proposals/NNNN-random-uuid.md @@ -0,0 +1,57 @@ +# Generating UUIDs using RandomNumberGenerators + +* Proposal: [SF-NNNN](NNNN-random-uuid.md) +* Authors: [FranzBusch](https://github.com/FranzBusch) +* Review Manager: TBD +* Status: **Awaiting review** +* Implementation: [swiftlang/swift-foundation#1271](https://github.com/swiftlang/swift-foundation/pull/1271) +* Review: ([pitch](https://forums.swift.org/...)) + +## Introduction + +UUIDs (Universally Unique IDentifiers) are 128 bits long and is intended to +guarantee uniqueness across space and time. This proposal adds APIs to generate +UUIDs from Swift's random number generators. + +## Motivation + +UUIDs often need to be randomly generated. This is currently possible by calling +the `UUID` initializer. However, this initializer doesn't allow providing a +custom source from which the `UUID` is generated. Swift's standard library +provides a common abstraction for random number generators through the +`RandomNumberGenerator` protocol. Providing methods to generate `UUID`s using a +`RandomNumberGenerator` allows developers to customize their source of randomness. + +An example where this is useful is where a system needs to generate UUIDs using a +deterministically seeded random number generator. + +## Proposed solution + +This proposal adds a new static method to the `UUID` type to generate new random `UUIDs` using a `RandomNumberGenerator`. + +```swift +/// Generates a new random UUID. +/// +/// - Parameter generator: The random number generator to use when creating the new random value. +/// - Returns: A random UUID. +@available(FoundationPreview 6.2, *) +public static func random( + using generator: inout some RandomNumberGenerator +) -> UUID +``` + +## Source compatibility + +The new API is purely additive and ha no impact on the existing API. + +## Implications on adoption + +This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source compatibility. + +## Alternatives considered + +### Initializer based random UUID generation + +The existing `UUID.init()` is already generating new random `UUID`s and a new +`UUID(using: &rng)` method would be a good alternative to the proposed static method. +However, the static `random` method has precedence on various types such as [Int.random](https://developer.apple.com/documentation/swift/int/random(in:)-9mjpw). diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index 2a5278219..8d987b138 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -76,6 +76,50 @@ public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable { hasher.combine(bytes: buffer) } } + + /// Generates a new random UUID. + /// + /// - Parameter generator: The random number generator to use when creating the new random value. + /// - Returns: A random UUID. + @available(FoundationPreview 6.2, *) + public static func random( + using generator: inout some RandomNumberGenerator + ) -> UUID { + let first = UInt64.random(in: .min ... .max, using: &generator) + let second = UInt64.random(in: .min ... .max, using: &generator) + + var firstBits = first + var secondBits = second + + // Set the version to 4 (0100 in binary) + firstBits &= 0xFFFFFFFFFFFF0FFF // Clear the last 12 bits + firstBits |= 0x0000000000004000 // Set the version bits to '0100' at the correct position + + // Set the variant to '10' (RFC9562 variant) + secondBits &= 0x3FFFFFFFFFFFFFFF // Clear the 2 most significant bits + secondBits |= 0x8000000000000000 // Set the two MSB to '10' + + let uuidBytes = ( + UInt8(truncatingIfNeeded: firstBits >> 56), + UInt8(truncatingIfNeeded: firstBits >> 48), + UInt8(truncatingIfNeeded: firstBits >> 40), + UInt8(truncatingIfNeeded: firstBits >> 32), + UInt8(truncatingIfNeeded: firstBits >> 24), + UInt8(truncatingIfNeeded: firstBits >> 16), + UInt8(truncatingIfNeeded: firstBits >> 8), + UInt8(truncatingIfNeeded: firstBits), + UInt8(truncatingIfNeeded: secondBits >> 56), + UInt8(truncatingIfNeeded: secondBits >> 48), + UInt8(truncatingIfNeeded: secondBits >> 40), + UInt8(truncatingIfNeeded: secondBits >> 32), + UInt8(truncatingIfNeeded: secondBits >> 24), + UInt8(truncatingIfNeeded: secondBits >> 16), + UInt8(truncatingIfNeeded: secondBits >> 8), + UInt8(truncatingIfNeeded: secondBits) + ) + + return UUID(uuid: uuidBytes) + } public var description: String { return uuidString diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index 0e9127621..eb0886a54 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -124,4 +124,23 @@ private struct UUIDTests { #expect(uuid2 <= uuid1) #expect(uuid2 == uuid1) } + + func testRandomVersionAndVariant() { + var generator = SystemRandomNumberGenerator() + for _ in 0..<10000 { + let uuid = UUID.random(using: &generator) + XCTAssertEqual(uuid.versionNumber, 0b0100) + XCTAssertEqual(uuid.varint, 0b10) + } + } +} + +extension UUID { + fileprivate var versionNumber: Int { + Int(self.uuid.6 >> 4) + } + + fileprivate var varint: Int { + Int(self.uuid.8 >> 6 & 0b11) + } } From 6908af5b95a11a2c27d32fb52f156fdfbccd91f6 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 30 Apr 2025 11:05:00 +0200 Subject: [PATCH 2/5] Seeded deterministic UUID generation test --- .../FoundationEssentialsTests/UUIDTests.swift | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index eb0886a54..5110f84e9 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -133,6 +133,22 @@ private struct UUIDTests { XCTAssertEqual(uuid.varint, 0b10) } } + + func testDeterministicRandomGeneration() { + var generator = PCGRandomNumberGenerator(seed: 123456789) + + let firstUUID = UUID.random(using: &generator) + XCTAssertEqual(firstUUID, UUID(uuidString: "9492BAC4-F353-49E7-ACBB-A40941CA65DE")) + + let secondUUID = UUID.random(using: &generator) + XCTAssertEqual(secondUUID, UUID(uuidString: "392C44E5-EB3E-4455-85A7-AF9556722B9A")) + + let thirdUUID = UUID.random(using: &generator) + XCTAssertEqual(thirdUUID, UUID(uuidString: "9ABFCCE9-AA85-485C-9CBF-C62F0C8D1D1A")) + + let fourthUUID = UUID.random(using: &generator) + XCTAssertEqual(fourthUUID, UUID(uuidString: "2B29542E-F719-4D58-87B9-C6291ADD4541")) + } } extension UUID { @@ -144,3 +160,28 @@ extension UUID { Int(self.uuid.8 >> 6 & 0b11) } } + +fileprivate struct PCGRandomNumberGenerator: RandomNumberGenerator { + private static let multiplier: UInt128 = 47_026_247_687_942_121_848_144_207_491_837_523_525 + private static let increment: UInt128 = 117_397_592_171_526_113_268_558_934_119_004_209_487 + + private var state: UInt128 + + fileprivate init(seed: UInt64) { + self.state = UInt128(seed) + } + + fileprivate mutating func next() -> UInt64 { + self.state = self.state &* Self.multiplier &+ Self.increment + + return rotr64( + value: UInt64(truncatingIfNeeded: self.state &>> 64) ^ UInt64(truncatingIfNeeded: self.state), + rotation: UInt64(truncatingIfNeeded: self.state &>> 122) + ) + } + + private func rotr64(value: UInt64, rotation: UInt64) -> UInt64 { + (value &>> rotation) | value &<< ((~rotation &+ 1) & 63) + } +} + From 81ed5a278a1725913d1600ca46d9d0dd8f69cb42 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 30 Apr 2025 11:17:20 +0200 Subject: [PATCH 3/5] Fix code comment --- Sources/FoundationEssentials/UUID.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index 8d987b138..953b90581 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -92,7 +92,7 @@ public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable { var secondBits = second // Set the version to 4 (0100 in binary) - firstBits &= 0xFFFFFFFFFFFF0FFF // Clear the last 12 bits + firstBits &= 0xFFFFFFFFFFFF0FFF // Clear bits 48 through 51 firstBits |= 0x0000000000004000 // Set the version bits to '0100' at the correct position // Set the variant to '10' (RFC9562 variant) From deeaab4b4fdc36d939d513df41750c913d59991c Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 29 Jul 2025 10:04:34 +0200 Subject: [PATCH 4/5] Migrate to swift-testing --- Tests/FoundationEssentialsTests/UUIDTests.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index 5110f84e9..424886a55 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -125,29 +125,29 @@ private struct UUIDTests { #expect(uuid2 == uuid1) } - func testRandomVersionAndVariant() { + @Test func randomVersionAndVariant() { var generator = SystemRandomNumberGenerator() for _ in 0..<10000 { let uuid = UUID.random(using: &generator) - XCTAssertEqual(uuid.versionNumber, 0b0100) - XCTAssertEqual(uuid.varint, 0b10) + #expect(uuid.versionNumber == 0b0100) + #expect(uuid.varint == 0b10) } } - func testDeterministicRandomGeneration() { + @Test func deterministicRandomGeneration() { var generator = PCGRandomNumberGenerator(seed: 123456789) let firstUUID = UUID.random(using: &generator) - XCTAssertEqual(firstUUID, UUID(uuidString: "9492BAC4-F353-49E7-ACBB-A40941CA65DE")) + #expect(firstUUID == UUID(uuidString: "9492BAC4-F353-49E7-ACBB-A40941CA65DE")) let secondUUID = UUID.random(using: &generator) - XCTAssertEqual(secondUUID, UUID(uuidString: "392C44E5-EB3E-4455-85A7-AF9556722B9A")) + #expect(secondUUID == UUID(uuidString: "392C44E5-EB3E-4455-85A7-AF9556722B9A")) let thirdUUID = UUID.random(using: &generator) - XCTAssertEqual(thirdUUID, UUID(uuidString: "9ABFCCE9-AA85-485C-9CBF-C62F0C8D1D1A")) + #expect(thirdUUID == UUID(uuidString: "9ABFCCE9-AA85-485C-9CBF-C62F0C8D1D1A")) let fourthUUID = UUID.random(using: &generator) - XCTAssertEqual(fourthUUID, UUID(uuidString: "2B29542E-F719-4D58-87B9-C6291ADD4541")) + #expect(fourthUUID == UUID(uuidString: "2B29542E-F719-4D58-87B9-C6291ADD4541")) } } From 7e987cf2286dbc092441ff933aaa04a0b4a22062 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 30 Jul 2025 10:24:47 +0200 Subject: [PATCH 5/5] Update availability Co-authored-by: Tina L <49205802+itingliu@users.noreply.github.com> --- Package.swift | 2 +- Proposals/NNNN-random-uuid.md | 2 +- Sources/FoundationEssentials/UUID.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 8c8b5046a..c6f29a2f9 100644 --- a/Package.swift +++ b/Package.swift @@ -9,7 +9,7 @@ import CompilerPluginSupport let availabilityTags: [_Availability] = [ _Availability("FoundationPreview"), // Default FoundationPreview availability ] -let versionNumbers = ["6.0.2", "6.1", "6.2"] +let versionNumbers = ["6.0.2", "6.1", "6.2", "6.3"] // Availability Macro Utilities diff --git a/Proposals/NNNN-random-uuid.md b/Proposals/NNNN-random-uuid.md index 4a17eb8d7..8410ee13c 100644 --- a/Proposals/NNNN-random-uuid.md +++ b/Proposals/NNNN-random-uuid.md @@ -34,7 +34,7 @@ This proposal adds a new static method to the `UUID` type to generate new random /// /// - Parameter generator: The random number generator to use when creating the new random value. /// - Returns: A random UUID. -@available(FoundationPreview 6.2, *) +@available(FoundationPreview 6.3, *) public static func random( using generator: inout some RandomNumberGenerator ) -> UUID diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index 953b90581..6b9449ca0 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -81,7 +81,7 @@ public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable { /// /// - Parameter generator: The random number generator to use when creating the new random value. /// - Returns: A random UUID. - @available(FoundationPreview 6.2, *) + @available(FoundationPreview 6.3, *) public static func random( using generator: inout some RandomNumberGenerator ) -> UUID {