-
Notifications
You must be signed in to change notification settings - Fork 15
Add ArrayStorage. #23
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -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<T>. | ||||||
| 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<R>( | ||||||
| _ 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_<R>( | ||||||
| _ 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<ArrayHeader, Element> | ||||||
|
|
||||||
| /// 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 | ||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this the same as:
Suggested change
Or am I missing something? If so, I think the suggestion might be easier to read.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, they're the same. I don't have a strong opinion about which is easier to read; they each have strengths. If you have any conviction about this, I'll take your suggestion. But I wish I could write
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 for having an operator. I can put together a PR shortly, and I'll update this code as well.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could follow Pascal precedent and have |
||||||
| } | ||||||
| return r | ||||||
| } | ||||||
|
|
||||||
| /// Invokes `body` with the memory occupied by stored elements. | ||||||
| public func withUnsafeMutableBufferPointer<R>( | ||||||
| _ body: (inout UnsafeMutableBufferPointer<Element>) -> R | ||||||
| ) -> R { | ||||||
| access.withUnsafeMutablePointers { h, e in | ||||||
| var b = UnsafeMutableBufferPointer(start: e, count: h[0].count) | ||||||
|
saeta marked this conversation as resolved.
|
||||||
| 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_<R>( | ||||||
| _ 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. | ||||||
|
dabrahams marked this conversation as resolved.
|
||||||
| /// | ||||||
| /// Note: instances have reference semantics. | ||||||
| public final class ArrayStorage<Element>: | ||||||
| AnyArrayStorage, ArrayStorageImplementation | ||||||
| { | ||||||
| override public var implementation: AnyArrayStorageImplementation { self } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| //****************************************************************************** | ||
| // 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<T>. | ||
| 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<Element: Factoid>: | ||
| 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<Double>` for `News` to help verify that type erasure is | ||
| // working properly. | ||
| func error(latest: Tracked<Double>) -> Double { | ||
| return latest.value / denominator | ||
| } | ||
|
|
||
| static func < (lhs: Self, rhs: Self) -> Bool { | ||
| lhs.denominator < rhs.denominator | ||
| } | ||
| } | ||
|
|
||
| /// Returns a collection of `Factoids` with the given denominators. | ||
| func factoids(_ r: Range<Int>) -> LazyMapCollection<Range<Int>, 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<Truthy>.test_create() | ||
| } | ||
|
|
||
| func test_append() { | ||
| FactoidArrayStorage<Truthy>.test_append(source: factoids(0..<100)) | ||
| } | ||
|
|
||
| func test_typeErasedAppend() { | ||
| FactoidArrayStorage<Truthy>.test_append( | ||
| source: factoids(0..<100), typeErased: true) | ||
| } | ||
|
|
||
| func test_withUnsafeMutableBufferPointer() { | ||
| FactoidArrayStorage<Truthy>.test_withUnsafeMutableBufferPointer( | ||
| sortedSource: factoids(99..<199)) | ||
| } | ||
|
|
||
| func test_withUnsafeMutableRawBufferPointer() { | ||
| FactoidArrayStorage<Truthy>.test_withUnsafeMutableBufferPointer( | ||
| sortedSource: factoids(99..<199), raw: true) | ||
| } | ||
|
|
||
| func test_deinit() { | ||
| FactoidArrayStorage<Tracked<Truthy>>.test_deinit { | ||
| Tracked(Truthy(denominator: 2), track: $0) | ||
| } | ||
| } | ||
|
|
||
| func test_totalError() { | ||
| FactoidArrayStorage<Truthy>.test_totalError() | ||
| } | ||
|
|
||
| func test_typeErasedTotalError() { | ||
| FactoidArrayStorage<Truthy>.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), | ||
| ] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: in Swift, I associate
Arrays with value semantics, but I didn't see CoW machinery here... would it be better to call thisBufferStorageImplementationor maybeBufferImplementation?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
“Array” has the connotations I was looking for: contiguity and homogeneity. “Buffer” doesn't necessarily do that. Also, this follows the convention set in the standard library: “Storage” is a class that provides the storage (https://github.com/apple/swift/blob/master/stdlib/public/core/ContiguousArrayBuffer.swift#L71), “ContiguousArrayBuffer” is a struct that adds resizability, and “Array” wraps that to provide value semantics.
Happy to use some other terms if you're convinced they're better though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with following the precedent for now, and we can think harder about this over time. LGTM!