Skip to content
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
184 changes: 184 additions & 0 deletions Sources/PenguinStructures/ArrayStorage.swift
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 {

Copy link
Copy Markdown
Owner

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 this BufferStorageImplementation or maybe BufferImplementation?

Copy link
Copy Markdown
Collaborator Author

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.

Copy link
Copy Markdown
Owner

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!

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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the same as:

Suggested change
h[0].count = r + 1
h.pointee.count = r + 1

Or am I missing something? If so, I think the suggestion might be easier to read.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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 h*.count. And maybe we should have that operator in penguin internals.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could follow Pascal precedent and have h^.count. I think I prefer * tho.

}
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)
Comment thread
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.
Comment thread
dabrahams marked this conversation as resolved.
///
/// Note: instances have reference semantics.
public final class ArrayStorage<Element>:
AnyArrayStorage, ArrayStorageImplementation
{
override public var implementation: AnyArrayStorageImplementation { self }
}
167 changes: 167 additions & 0 deletions Tests/PenguinStructuresTests/ArrayStorageExtensionTests.swift
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),
]
}
Loading