diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75ef6bd..74d27e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,18 +13,23 @@ jobs: name: MacOS runs-on: macOS-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Run tests run: make test-swift - name: Run tests run: make build-release-swift ubuntu: - name: Ubuntu + strategy: + matrix: + swift: + - '5.9' + name: Ubuntu (Swift ${{ matrix.swift }}) runs-on: ubuntu-latest + container: swift:${{ matrix.swift }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Run tests - run: make test-linux - - name: Build for release - run: make build-release-linux + run: swift test --parallel + - name: Run tests (release) + run: swift test -c release --parallel diff --git a/Package.swift b/Package.swift index 55aa546..c727a75 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.0 +// swift-tools-version:5.9 import Foundation import PackageDescription @@ -8,7 +8,11 @@ let package = Package( .library(name: "Gen", targets: ["Gen"]) ], targets: [ - .target(name: "Gen", dependencies: []), + .target( + name: "Gen", + dependencies: [], + swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] + ), .testTarget(name: "GenTests", dependencies: ["Gen"]), ] ) diff --git a/Sources/Gen/Gen.swift b/Sources/Gen/Gen.swift index 04c072e..57b44c8 100644 --- a/Sources/Gen/Gen.swift +++ b/Sources/Gen/Gen.swift @@ -1,10 +1,10 @@ /// A composable, transformable context for generating random values. -public struct Gen { +public struct Gen: Sendable { @usableFromInline - internal var _run: (inout AnyRandomNumberGenerator) -> Value + internal var _run: @Sendable (inout AnyRandomNumberGenerator) -> Value @inlinable - public init(run: @escaping (inout AnyRandomNumberGenerator) -> Value) { + public init(run: @escaping @Sendable (inout AnyRandomNumberGenerator) -> Value) { self._run = run } @@ -48,31 +48,22 @@ extension Gen { /// - Parameter transform: A function that transforms `Value`s into `NewValue`s. /// - Returns: A generator of `NewValue`s. @inlinable - public func map(_ transform: @escaping (Value) -> NewValue) -> Gen { + public func map( + _ transform: @escaping @Sendable (Value) -> NewValue + ) -> Gen { return Gen { rng in transform(self._run(&rng)) } } } -/// Combines two generators into a single one. -/// -/// - Parameters: -/// - a: A generator of `A`s. -/// - b: A generator of `B`s. -/// - Returns: A generator of `(A, B)` pairs. -@inlinable -public func zip(_ a: Gen, _ b: Gen) -> Gen<(A, B)> { - return Gen<(A, B)> { rng in - (a._run(&rng), b._run(&rng)) - } -} - extension Gen { /// Transforms a generator of `Value`s into a generator of `NewValue`s by transforming a value into a generator of `NewValue`s. /// /// - Parameter transform: A function that transforms `Value`s into a generator of `NewValue`s. /// - Returns: A generator of `NewValue`s. @inlinable - public func flatMap(_ transform: @escaping (Value) -> Gen) -> Gen { + public func flatMap( + _ transform: @escaping @Sendable (Value) -> Gen + ) -> Gen { return Gen { rng in transform(self._run(&rng))._run(&rng) } @@ -83,7 +74,9 @@ extension Gen { /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value. /// - Returns: A generator of the non-nil results of calling the given transformation with a value of the generator. @inlinable - public func compactMap(_ transform: @escaping (Value) -> NewValue?) -> Gen { + public func compactMap( + _ transform: @escaping @Sendable (Value) -> NewValue? + ) -> Gen { return Gen { rng in while true { if let value = transform(self._run(&rng)) { @@ -98,7 +91,7 @@ extension Gen { /// - Parameter predicate: A predicate. /// - Returns: A generator of values that match the predicate. @inlinable - public func filter(_ predicate: @escaping (Value) -> Bool) -> Gen { + public func filter(_ predicate: @escaping @Sendable (Value) -> Bool) -> Gen { return self.compactMap { predicate($0) ? $0 : nil } } @@ -143,7 +136,7 @@ extension Gen where Value: BinaryFloatingPoint, Value.RawSignificand: FixedWidth } } -extension Gen where Value == Int { +extension Gen { /// Returns a generator of random values within the specified range. /// /// - Parameter range: The range in which to create a random value. `range` must be finite. @@ -154,7 +147,7 @@ extension Gen where Value == Int { } } -extension Gen where Value == Int8 { +extension Gen { /// Returns a generator of random values within the specified range. /// /// - Parameter range: The range in which to create a random value. `range` must be finite. @@ -165,7 +158,7 @@ extension Gen where Value == Int8 { } } -extension Gen where Value == Int16 { +extension Gen { /// Returns a generator of random values within the specified range. /// /// - Parameter range: The range in which to create a random value. `range` must be finite. @@ -176,7 +169,7 @@ extension Gen where Value == Int16 { } } -extension Gen where Value == Int32 { +extension Gen { /// Returns a generator of random values within the specified range. /// /// - Parameter range: The range in which to create a random value. `range` must be finite. @@ -187,7 +180,7 @@ extension Gen where Value == Int32 { } } -extension Gen where Value == Int64 { +extension Gen { /// Returns a generator of random values within the specified range. /// /// - Parameter range: The range in which to create a random value. `range` must be finite. @@ -198,7 +191,7 @@ extension Gen where Value == Int64 { } } -extension Gen where Value == UInt { +extension Gen { /// Returns a generator of random values within the specified range. /// /// - Parameter range: The range in which to create a random value. `range` must be finite. @@ -209,7 +202,7 @@ extension Gen where Value == UInt { } } -extension Gen where Value == UInt8 { +extension Gen { /// Returns a generator of random values within the specified range. /// /// - Parameter range: The range in which to create a random value. `range` must be finite. @@ -220,7 +213,7 @@ extension Gen where Value == UInt8 { } } -extension Gen where Value == UInt16 { +extension Gen { /// Returns a generator of random values within the specified range. /// /// - Parameter range: The range in which to create a random value. `range` must be finite. @@ -231,7 +224,7 @@ extension Gen where Value == UInt16 { } } -extension Gen where Value == UInt32 { +extension Gen { /// Returns a generator of random values within the specified range. /// /// - Parameter range: The range in which to create a random value. `range` must be finite. @@ -242,7 +235,7 @@ extension Gen where Value == UInt32 { } } -extension Gen where Value == UInt64 { +extension Gen { /// Returns a generator of random values within the specified range. /// /// - Parameter range: The range in which to create a random value. `range` must be finite. @@ -253,7 +246,7 @@ extension Gen where Value == UInt64 { } } -extension Gen where Value == Double { +extension Gen { /// Returns a generator of random values within the specified range. /// /// - Parameter range: The range in which to create a random value. `range` must be finite. @@ -264,7 +257,7 @@ extension Gen where Value == Double { } } -extension Gen where Value == Float { +extension Gen { /// Returns a generator of random values within the specified range. /// /// - Parameter range: The range in which to create a random value. `range` must be finite. @@ -276,7 +269,7 @@ extension Gen where Value == Float { } #if !(os(Windows) || os(Android)) && (arch(i386) || arch(x86_64)) - extension Gen where Value == Float80 { + extension Gen { /// Returns a generator of random values within the specified range. /// /// - Parameter range: The range in which to create a random value. `range` must be finite. @@ -291,7 +284,7 @@ extension Gen where Value == Float { #if canImport(CoreGraphics) import CoreGraphics - extension Gen where Value == CGFloat { + extension Gen { /// Returns a generator of random values within the specified range. /// /// - Parameter range: The range in which to create a random value. `range` must be finite. @@ -303,7 +296,7 @@ extension Gen where Value == Float { } #endif -extension Gen where Value == Bool { +extension Gen { /// A generator of random boolean values. public static let bool = Gen { rng in Bool.random(using: &rng) } } @@ -313,7 +306,8 @@ extension Gen { /// /// - Parameter collection: A collection. @inlinable - public static func element(of collection: C) -> Gen where C: Collection, Value == C.Element? { + public static func element(of collection: C) -> Gen + where C: Collection & Sendable, Value == C.Element? { return Gen { rng in collection.randomElement(using: &rng) } } @@ -321,26 +315,27 @@ extension Gen { /// /// - Parameter collection: A collection. @inlinable - public static func shuffled(_ collection: C) -> Gen where C: Collection, Value == [C.Element] { + public static func shuffled(_ collection: C) -> Gen + where C: Collection & Sendable, Value == [C.Element] { return Gen { rng in collection.shuffled(using: &rng) } } } -extension Gen where Value: Collection { +extension Gen where Value: Collection, Value.Element: Sendable { /// Produces a generator of random elements of this generator's collection. @inlinable public var element: Gen { - return self.flatMap(Gen.element) + self.flatMap { Gen.element(of: $0) } } /// Produces a generator of shuffled arrays of this generator's collection. @inlinable public var shuffled: Gen<[Value.Element]> { - return self.flatMap(Gen<[Value.Element]>.shuffled) + self.flatMap { Gen<[Value.Element]>.shuffled($0) } } } -extension Gen where Value: CaseIterable { +extension Gen where Value: CaseIterable, Value.AllCases: Sendable { /// Produces a generator of all case-iterable cases. @inlinable public static var allCases: Gen { @@ -369,7 +364,7 @@ extension Gen { } } } - + /// Produces a new generator of arrays of this generator's values. /// /// - Parameter count: The size of the random array. @@ -388,7 +383,7 @@ extension Gen { } } } - + /// Produces a new generator of dictionaries of this generator's pairs. /// /// - Parameter count: The size of the random dictionary. @@ -408,7 +403,7 @@ extension Gen { } } } - + /// Produces a new generator of sets of this generator's values. /// /// - Parameter count: The size of the random set. @@ -426,7 +421,9 @@ extension Gen { } } } +} +extension Gen { /// Produces a new generator of optional values. /// /// - Returns: A generator of optional values. @@ -434,7 +431,7 @@ extension Gen { public var optional: Gen { return Gen.frequency( (1, Gen.always(Value?.none)), - (3, self.map(Value?.some)) // TODO: Change to use `size` with resizable generators? + (3, self.map { Value?.some($0) }) // TODO: Change to use `size` with resizable generators? ) } @@ -444,8 +441,8 @@ extension Gen { @inlinable public func asResult(withFailure gen: Gen) -> Gen> { return Gen>.frequency( - (1, gen.map(Result.failure)), - (3, self.map(Result.success)) // TODO: Change to use `size` with resizable generators? + (1, gen.map { Result.failure($0) }), + (3, self.map { Result.success($0) }) // TODO: Change to use `size` with resizable generators? ) } } @@ -470,7 +467,7 @@ extension Gen where Value: Hashable { } } -extension Gen where Value == UnicodeScalar { +extension Gen { /// Returns a generator of random unicode scalars within the specified range. /// /// - Parameter range: The range in which to create a random unicode scalar. `range` must be finite. @@ -483,7 +480,7 @@ extension Gen where Value == UnicodeScalar { } } -extension Gen where Value == Character { +extension Gen { // FIXME: Make safe for characters with multiple scalars. /// Returns a generator of random characters within the specified range. /// @@ -495,7 +492,7 @@ extension Gen where Value == Character { .unicodeScalar( in: range.lowerBound.unicodeScalars.first!...range.upperBound.unicodeScalars.last! ) - .map(Character.init) + .map { Character($0) } } /// A generator of random numeric digits. @@ -533,17 +530,22 @@ extension Gen where Value == Character { /// - Returns: A generator of strings. @inlinable public func string(of count: Gen) -> Gen { - return self.map(String.init).array(of: count).map { $0.joined() } + return self + .map { String($0) } + .array(of: count) + .map { $0.joined() } } } -extension Sequence { +extension Sequence where Self: Sendable, Element: Sendable { /// Transforms each value of an array of generators before rewrapping the array in an array generator. /// /// - Parameter transform: A transform function to apply to the value of each generator. /// - Returns: A generator of arrays. @inlinable - public func traverse(_ transform: @escaping (A) -> B) -> Gen<[B]> where Element == Gen { + public func traverse( + _ transform: @escaping @Sendable (A) -> B + ) -> Gen<[B]> where Element == Gen { return Gen<[B]> { rng in self.map { transform($0.run(using: &rng)) } } diff --git a/Sources/Gen/Zip.swift b/Sources/Gen/Zip.swift index 9fa3095..3c6839c 100644 --- a/Sources/Gen/Zip.swift +++ b/Sources/Gen/Zip.swift @@ -1,3 +1,16 @@ +/// Combines two generators into a single one. +/// +/// - Parameters: +/// - a: A generator of `A`s. +/// - b: A generator of `B`s. +/// - Returns: A generator of `(A, B)` pairs. +@inlinable +public func zip(_ a: Gen, _ b: Gen) -> Gen<(A, B)> { + return Gen<(A, B)> { rng in + (a._run(&rng), b._run(&rng)) + } +} + @inlinable public func zip( _ a: Gen, diff --git a/Tests/GenTests/GenTests.swift b/Tests/GenTests/GenTests.swift index 757aa38..c0fe07e 100644 --- a/Tests/GenTests/GenTests.swift +++ b/Tests/GenTests/GenTests.swift @@ -5,7 +5,7 @@ final class GenTests: XCTestCase { var xoshiro = Xoshiro(seed: 0) func testMap() { - let gen = Gen.bool.map(String.init) + let gen = Gen.bool.map { String($0) } XCTAssertEqual("false", gen.run(using: &xoshiro)) } @@ -101,7 +101,7 @@ final class GenTests: XCTestCase { } func testTraverse() { - let gen = [Gen.int(in: 1...100), Gen.int(in: 1_000...1_000_000)].traverse(String.init) + let gen = [Gen.int(in: 1...100), Gen.int(in: 1_000...1_000_000)].traverse { String($0) } XCTAssertEqual(["15", "18473"], gen.run(using: &xoshiro)) }