From bebf1bfc34ffbee6779da3f5df42b5d78b1e8a9c Mon Sep 17 00:00:00 2001 From: Dave Abrahams Date: Wed, 20 May 2020 10:21:53 -0700 Subject: [PATCH 1/2] Add ArrayStorage. --- Sources/PenguinStructures/ArrayStorage.swift | 184 ++++++++++++++++++ .../ArrayStorageExtensionTests.swift | 142 ++++++++++++++ .../ArrayStorageTests.swift | 139 +++++++++++++ Tests/PenguinStructuresTests/Tracked.swift | 41 ++++ 4 files changed, 506 insertions(+) create mode 100644 Sources/PenguinStructures/ArrayStorage.swift create mode 100644 Tests/PenguinStructuresTests/ArrayStorageExtensionTests.swift create mode 100644 Tests/PenguinStructuresTests/ArrayStorageTests.swift create mode 100644 Tests/PenguinStructuresTests/Tracked.swift diff --git a/Sources/PenguinStructures/ArrayStorage.swift b/Sources/PenguinStructures/ArrayStorage.swift new file mode 100644 index 00000000..78d2db1c --- /dev/null +++ b/Sources/PenguinStructures/ArrayStorage.swift @@ -0,0 +1,184 @@ +//****************************************************************************** +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +//****************************************************************************** +// This file exposes reference-counted buffer types similar to the storage for +// `Array`, but designed to be (potentially) handled through type-erased APIs +// that are statically `Element`-type-agnostic. +//****************************************************************************** + +/// Effectively, the stored properties of every `ArrayStorage` instance. +fileprivate struct ArrayHeader { + var count: Int + var capacity: Int +} + +/// Contiguous storage of homogeneous elements of statically unknown type. +/// +/// This class provides the element-type-agnostic API for ArrayStorage. +open class AnyArrayStorage { + /// An instance that provides the implementation of element-type-agnostic APIs + /// defined in this class. + open var implementation: AnyArrayStorageImplementation { + fatalError("implement me!") + } + + /// Appends the instance of the concrete element type whose address is `p`, + /// returning the index of the appended element, or `nil` if there was + /// insufficient capacity remaining + public final func appendValue(at p: UnsafeRawPointer) -> Int? { + implementation.appendValue_(at: p) + } + + /// Invokes `body` with the memory occupied by initialized elements. + public final func withUnsafeMutableRawBufferPointer( + _ body: (inout UnsafeMutableRawBufferPointer)->R + ) -> R { + implementation.withUnsafeMutableRawBufferPointer_(body) + } + + /// Deinitializes all stored data. + deinit { implementation.deinitialize() } +} + +/// Contiguous storage of homogeneous elements of statically unknown type. +/// +/// Conformances to this protocol provide the implementations for +/// `AnyArrayStorage` APIs. +public protocol AnyArrayStorageImplementation: AnyArrayStorage { + /// Appends the instance of the concrete element type whose address is `p`, + /// returning the index of the appended element, or `nil` if there was + /// insufficient capacity remaining + func appendValue_(at p: UnsafeRawPointer) -> Int? + + /// Invokes `body` with the memory occupied by initialized elements. + func withUnsafeMutableRawBufferPointer_( + _ body: (inout UnsafeMutableRawBufferPointer)->R + ) -> R + + /// Deinitialize stored data + func deinitialize() +} + +/// Contiguous storage of homogeneous elements of statically known type. +/// +/// This protocol's extensions provide APIs that depend on the element type, and +/// the implementations for `AnyArrayStorage` APIs. +public protocol ArrayStorageImplementation: AnyArrayStorageImplementation { + associatedtype Element +} + +/// APIs that depend on the `Element` type. +extension ArrayStorageImplementation { + /// A type whose instances can be used to access the memory of `self` with a + /// degree of type-safety. + private typealias Accessor = ManagedBufferPointer + + /// A handle to the memory of `self` providing a degree of type-safety. + private var access: Accessor { .init(unsafeBufferObject: self) } + + /// The number of elements stored in `self`. + public var count: Int { + _read { yield access.withUnsafeMutablePointerToHeader { $0.pointee.count } } + _modify { + defer { _fixLifetime(self) } + yield &access.withUnsafeMutablePointerToHeader { $0 }.pointee.count + } + } + + /// The maximum number of elements that can be stored in `self`. + public var capacity: Int { + _read { + yield access.withUnsafeMutablePointerToHeader { + $0.pointee.capacity } + } + _modify { + defer { _fixLifetime(self) } + yield &access.withUnsafeMutablePointerToHeader { $0 }.pointee.capacity + } + } + + /// Appends `x` if possible, returning the index of the appended element or + /// `nil` if there was insufficient capacity remaining. + public func append(_ x: Element) -> Int? { + let r = count + if r == capacity { return nil } + access.withUnsafeMutablePointers { h, e in + (e + r).initialize(to: x) + h[0].count = r + 1 + } + return r + } + + /// Invokes `body` with the memory occupied by stored elements. + public func withUnsafeMutableBufferPointer( + _ body: (inout UnsafeMutableBufferPointer) -> R + ) -> R { + access.withUnsafeMutablePointers { h, e in + var b = UnsafeMutableBufferPointer(start: e, count: h[0].count) + return body(&b) + } + } + + /// Returns an empty instance with `capacity` at least `minimumCapacity`. + public static func create(minimumCapacity: Int) -> Self { + unsafeDowncast( + Accessor(bufferClass: Self.self, minimumCapacity: minimumCapacity) { + buffer, getCapacity in + ArrayHeader(count: 0, capacity: getCapacity(buffer)) + }.buffer, + to: Self.self) + } + + private init() { fatalError("Please call create()") } +} + +/// Implementation of `AnyArrayStorageImplementation` requirements +extension ArrayStorageImplementation { + /// Appends the instance of the concrete element type whose address is `p`, + /// returning the index of the appended element, or `nil` if there was + /// insufficient capacity remaining + public func appendValue_(at p: UnsafeRawPointer) -> Int? { + append(p.assumingMemoryBound(to: Element.self)[0]) + } + + /// Invokes `body` with the memory occupied by initialized elements. + public func withUnsafeMutableRawBufferPointer_( + _ body: (inout UnsafeMutableRawBufferPointer)->R + ) -> R { + withUnsafeMutableBufferPointer { + var b = UnsafeMutableRawBufferPointer($0) + return body(&b) + } + } + + /// Deinitialize stored data. Models should call this from their `deinit`. + public func deinitialize() { + access.withUnsafeMutablePointers { h, e in + e.deinitialize(count: h[0].count) + h.deinitialize(count: 1) + } + } +} + +/// Type-erasable storage for contiguous `Factoid` `Element` instances. +/// +/// Note: instances have reference semantics. +public final class ArrayStorage: + AnyArrayStorage, ArrayStorageImplementation +{ + override public var implementation: AnyArrayStorageImplementation { self } +} diff --git a/Tests/PenguinStructuresTests/ArrayStorageExtensionTests.swift b/Tests/PenguinStructuresTests/ArrayStorageExtensionTests.swift new file mode 100644 index 00000000..82ea392c --- /dev/null +++ b/Tests/PenguinStructuresTests/ArrayStorageExtensionTests.swift @@ -0,0 +1,142 @@ +//****************************************************************************** +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import XCTest +import PenguinStructures + +/// Types that may represent erroneous statements. +/// +/// Functionality depending on `Factoid` is built into the type-erased storage +/// tested in this file. +protocol Factoid { + /// A type that represents news relevant to this factoid. + associatedtype News + + /// Returns a value describing just how erroneous `self` is, given today's + /// news. + func error(latest: News) -> Double +} + +extension Tracked: Factoid where T: Factoid { + func error(latest: T.News) -> Double { value.error(latest: latest) } +} + +/// Contiguous storage of homogeneous `Factoid`s of statically unknown type. +/// +/// This class provides the element-type-agnostic API for FactoidArrayStorage. +class AnyFactoidArrayStorage: AnyArrayStorage { + typealias FactoidImplementation = AnyFactoidArrayStorageImplementation + var factoidImplementation: FactoidImplementation { + fatalError("implement me!") + } + + /// Returns the sum of all stored errors, given the address of today's news. + final func totalError(latestAt p: UnsafeRawPointer) -> Double { + factoidImplementation.totalError_(latestAt: p) + } +} + +/// Contiguous storage of homogeneous `Factoid`s of statically unknown type. +/// +/// Conformances to this protocol provide the implementations for +/// `AnyFactoidArrayStorage` APIs. +protocol AnyFactoidArrayStorageImplementation: AnyFactoidArrayStorage { + func totalError_(latestAt p: UnsafeRawPointer) -> Double +} + +/// APIs that depend on the `Factoid` `Element` type. +extension ArrayStorageImplementation where Element: Factoid { + func totalError(latest: Element.News) -> Double { + withUnsafeMutableBufferPointer { elements in + elements.reduce(0.0) { $0 + $1.error(latest: latest) } + } + } + + func totalError_(latestAt p: UnsafeRawPointer) -> Double { + totalError(latest: p.assumingMemoryBound(to: Element.News.self)[0]) + } +} + +/// Type-erasable storage for contiguous `Factoid` `Element` instances. +/// +/// Note: instances have reference semantics. +final class FactoidArrayStorage: + AnyFactoidArrayStorage, AnyFactoidArrayStorageImplementation, + ArrayStorageImplementation +{ + override var implementation: AnyArrayStorageImplementation { self } + override var factoidImplementation: AnyFactoidArrayStorageImplementation { self } +} + +/// A sample Factoid we can use for testing. +struct Truthy: Factoid, Comparable { + var denominator: Double + + // Using `Tracked` for `News` to help verify that type erasure is + // working properly. + func error(latest: Tracked) -> Double { + return latest.value / denominator + } + + static func < (lhs: Self, rhs: Self) -> Bool { + lhs.denominator < rhs.denominator + } +} + +class test_ExtendedStorage: XCTestCase { + final func factoids(_ r: Range) -> LazyMapCollection, Truthy> { + r.lazy.map { Truthy(denominator: Double($0)) } + } + + func test_create() { + FactoidArrayStorage.test_create() + } + + func test_append() { + FactoidArrayStorage.test_append(source: factoids(0..<100)) + } + + func test_typeErasedAppend() { + FactoidArrayStorage.test_append( + source: factoids(0..<100), typeErased: true) + } + + func test_withUnsafeMutableBufferPointer() { + FactoidArrayStorage.test_withUnsafeMutableBufferPointer( + sortedSource: factoids(99..<199)) + } + + func test_withUnsafeMutableRawBufferPointer() { + FactoidArrayStorage.test_withUnsafeMutableBufferPointer( + sortedSource: factoids(99..<199), raw: true) + } + + func test_deinit() { + FactoidArrayStorage>.test_deinit { + Tracked(Truthy(denominator: 2), track: $0) + } + } + + func test_totalError() { + let s = FactoidArrayStorage.create(minimumCapacity: 10) + for f in factoids(0..<10) { _ = s.append(f) } + let latest = Tracked(0.5) { _ in } + let total = withUnsafePointer(to: latest) { + s.totalError(latestAt: $0) + } + let expected = (0..<10).reduce(0.0) { $0 + 0.5 / Double($1) } + XCTAssertEqual(total, expected) + } +} diff --git a/Tests/PenguinStructuresTests/ArrayStorageTests.swift b/Tests/PenguinStructuresTests/ArrayStorageTests.swift new file mode 100644 index 00000000..85b532e3 --- /dev/null +++ b/Tests/PenguinStructuresTests/ArrayStorageTests.swift @@ -0,0 +1,139 @@ +//****************************************************************************** +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import XCTest +import PenguinStructures + +// Reusable test implementations + +extension ArrayStorageImplementation { + static func test_create() { + for n in 0..<100 { + let s = Self.create(minimumCapacity: n) + XCTAssertEqual(s.count, 0) + XCTAssertGreaterThanOrEqual(s.capacity, n) + } + } + + /// tests deinit. + /// + /// Parameter newElement: given a tracking closure with semantics suitable for + /// `Tracked` above, returns a new element. + static func test_deinit(_ newElement: (@escaping (Int)->Void)->Element) { + var count = 0 + do { + let s = Self.create(minimumCapacity: 100) + for _ in 0..<100 { _ = s.append(newElement { count += $0 }) } + XCTAssertEqual(count, 100) // sanity check + + // Keep s alive until we've tested count above + XCTAssertEqual(s.count, 100) + } + XCTAssertEqual( + count, 0, + "deinit failed to deinitialize some stored elements." + ) + } +} + +extension ArrayStorageImplementation where Element: Equatable { + /// Tests `append`, or if `typeErased == true`, `appendValue(at:)`. + static func test_append( + source: Source, typeErased: Bool = false + ) + where Source.Element == Element + { + for n in 0.. Int? { + typeErased + ? withUnsafePointer(to: e) { s.appendValue(at: .init($0)) } + : s.append(e) + } + + for (i, e) in source.prefix(n).enumerated() { + XCTAssertEqual(s.count, i) + let newPosition = doAppend(e) + XCTAssertEqual(newPosition, i) + XCTAssertEqual(s.count, i + 1) + XCTAssertEqual(s.withUnsafeMutableBufferPointer { $0.last }, e) + } + // Ensure we can fill up any remaining capacity + while s.count < s.capacity { + let newPosition = doAppend(source.first!) + XCTAssertEqual(newPosition, s.count - 1) + } + } + } +} + +extension ArrayStorageImplementation where Element: Comparable { + /// Tests `withUnsafeMutableBufferPointer`, or if `raw == true`, + /// `withUnsafeMutableRawBufferPointer`. + static func test_withUnsafeMutableBufferPointer( + sortedSource: Source, raw: Bool = false + ) where Source.Element == Element { + let s = Self.create(minimumCapacity: sortedSource.count) + for i in sortedSource.reversed() { _ = s.append(i) } + + typealias TypedBuffer = UnsafeMutableBufferPointer + + func withBuffer(_ body: (inout TypedBuffer)->R) -> R { + return raw + ? s.withUnsafeMutableRawBufferPointer { rawBuffer in + var b = TypedBuffer( + start: rawBuffer.baseAddress?.assumingMemoryBound(to: Element.self), + count: rawBuffer.count / MemoryLayout.stride) + return body(&b) + } + : s.withUnsafeMutableBufferPointer(body) + } + + XCTAssertFalse(withBuffer { $0.elementsEqual(sortedSource) }) + withBuffer { $0.sort() } + XCTAssert(withBuffer { $0.elementsEqual(sortedSource) }) + } +} + +class test_ArrayStorage: XCTestCase { + func test_create() { + ArrayStorage.test_create() + } + + func test_append() { + ArrayStorage.test_append( + source: (0..<100).lazy.map { UInt8($0) }) + } + + func test_typeErasedAppend() { + ArrayStorage.test_append( + source: (0..<100).lazy.map { UInt8($0) }, typeErased: true) + } + + func test_withUnsafeMutableBufferPointer() { + ArrayStorage.test_withUnsafeMutableBufferPointer( + sortedSource: 99..<199) + } + + func test_withUnsafeMutableRawBufferPointer() { + ArrayStorage.test_withUnsafeMutableBufferPointer( + sortedSource: 99..<199, raw: true) + } + + func test_deinit() { + ArrayStorage>.test_deinit { Tracked((), track: $0) } + } +} diff --git a/Tests/PenguinStructuresTests/Tracked.swift b/Tests/PenguinStructuresTests/Tracked.swift new file mode 100644 index 00000000..eba0fdc9 --- /dev/null +++ b/Tests/PenguinStructuresTests/Tracked.swift @@ -0,0 +1,41 @@ +//****************************************************************************** +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +/// A wrapper over an arbitrary type that can be used to count instances and +/// make sure they're being properly disposed of. +final class Tracked { + /// The wrapped value. + var value: T + + /// An arbitrary counter of instances. + /// + /// This function that is passed 1 for each instance of `self` created, and -1 + /// for each instance destroyed. + let track: (Int) -> Void + + /// Creates an instance holding `value` and invoking `track` to count + /// instances. + /// + /// - Parameter track: called with 1 for each instance of `self` created, and + /// -1 for each instance destroyed. + init(_ value: T, track: @escaping (Int)->Void) { + self.value = value + self.track = track + track(1) + } + + deinit { track(-1) } +} From 3ca12e60f248c77079c9ebacb73ca300de91da7c Mon Sep 17 00:00:00 2001 From: Dave Abrahams Date: Wed, 20 May 2020 13:10:30 -0700 Subject: [PATCH 2/2] Update XCTest manifest. --- .../ArrayStorageExtensionTests.swift | 47 ++++++++++++++----- .../ArrayStorageTests.swift | 13 ++++- .../XCTestManifests.swift | 2 + 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/Tests/PenguinStructuresTests/ArrayStorageExtensionTests.swift b/Tests/PenguinStructuresTests/ArrayStorageExtensionTests.swift index 82ea392c..be25be7c 100644 --- a/Tests/PenguinStructuresTests/ArrayStorageExtensionTests.swift +++ b/Tests/PenguinStructuresTests/ArrayStorageExtensionTests.swift @@ -95,11 +95,26 @@ struct Truthy: Factoid, Comparable { } } -class test_ExtendedStorage: XCTestCase { - final func factoids(_ r: Range) -> LazyMapCollection, Truthy> { +/// Returns a collection of `Factoids` with the given denominators. +func factoids(_ r: Range) -> LazyMapCollection, Truthy> { r.lazy.map { Truthy(denominator: Double($0)) } +} + +extension FactoidArrayStorage where Element == Truthy { + static func test_totalError(typeErased: Bool = false) { + let s = self.create(minimumCapacity: 10) + for f in factoids(0..<10) { _ = s.append(f) } + let latest = Tracked(0.5) { _ in } + let total = typeErased + ? withUnsafePointer(to: latest) { s.totalError(latestAt: $0) } + : s.totalError(latest: latest) + + let expected = (0..<10).reduce(0.0) { $0 + 0.5 / Double($1) } + XCTAssertEqual(total, expected) } - +} + +class ArrayStorageExtensionTests: XCTestCase { func test_create() { FactoidArrayStorage.test_create() } @@ -130,13 +145,23 @@ class test_ExtendedStorage: XCTestCase { } func test_totalError() { - let s = FactoidArrayStorage.create(minimumCapacity: 10) - for f in factoids(0..<10) { _ = s.append(f) } - let latest = Tracked(0.5) { _ in } - let total = withUnsafePointer(to: latest) { - s.totalError(latestAt: $0) - } - let expected = (0..<10).reduce(0.0) { $0 + 0.5 / Double($1) } - XCTAssertEqual(total, expected) + FactoidArrayStorage.test_totalError() + } + + func test_typeErasedTotalError() { + FactoidArrayStorage.test_totalError(typeErased: true) } + + static var allTests = [ + ("test_create", test_create), + ("test_append", test_append), + ("test_typeErasedAppend", test_typeErasedAppend), + ("test_withUnsafeMutableBufferPointer", test_withUnsafeMutableBufferPointer), + ( + "test_withUnsafeMutableRawBufferPointer", + test_withUnsafeMutableRawBufferPointer), + ("test_deinit", test_deinit), + ("test_totalError", test_totalError), + ("test_typeErasedTotalError", test_typeErasedTotalError), + ] } diff --git a/Tests/PenguinStructuresTests/ArrayStorageTests.swift b/Tests/PenguinStructuresTests/ArrayStorageTests.swift index 85b532e3..4efb2f1a 100644 --- a/Tests/PenguinStructuresTests/ArrayStorageTests.swift +++ b/Tests/PenguinStructuresTests/ArrayStorageTests.swift @@ -108,7 +108,7 @@ extension ArrayStorageImplementation where Element: Comparable { } } -class test_ArrayStorage: XCTestCase { +class ArrayStorageTests: XCTestCase { func test_create() { ArrayStorage.test_create() } @@ -136,4 +136,15 @@ class test_ArrayStorage: XCTestCase { func test_deinit() { ArrayStorage>.test_deinit { Tracked((), track: $0) } } + + static var allTests = [ + ("test_create", test_create), + ("test_append", test_append), + ("test_typeErasedAppend", test_typeErasedAppend), + ("test_withUnsafeMutableBufferPointer", test_withUnsafeMutableBufferPointer), + ( + "test_withUnsafeMutableRawBufferPointer", + test_withUnsafeMutableRawBufferPointer), + ("test_deinit", test_deinit), + ] } diff --git a/Tests/PenguinStructuresTests/XCTestManifests.swift b/Tests/PenguinStructuresTests/XCTestManifests.swift index 817fb2f3..05fc507b 100644 --- a/Tests/PenguinStructuresTests/XCTestManifests.swift +++ b/Tests/PenguinStructuresTests/XCTestManifests.swift @@ -21,6 +21,8 @@ import XCTest testCase(DoubleEndedBufferTests.allTests), testCase(HeapTests.allTests), testCase(HierarchicalCollectionTests.allTests), + testCase(ArrayStorageTests.allTests), + testCase(ArrayStorageExtensionTests.allTests), ] } #endif