diff --git a/Sources/BasicContainers/BasicContainers.docc/BasicContainers.md b/Sources/BasicContainers/BasicContainers.docc/BasicContainers.md index d10b36061..85bcb7241 100644 --- a/Sources/BasicContainers/BasicContainers.docc/BasicContainers.md +++ b/Sources/BasicContainers/BasicContainers.docc/BasicContainers.md @@ -60,9 +60,35 @@ Unlike ``InlineArray``, the capacity of a ``RigidArray`` is not part of its type This allows ``RigidArray`` to still provide _explicit_ resizing operations: it has a `reallocate(capacity:)` method that can be used to arbitrarily resize its storage, as well as the familiar ``reserveCapacity(_:)`` operation. This enables building dynamic array types on top of ``RigidArray``; indeed, `UniqueArray` is a relatively simple wrapper around rigid array instance, forwarding operations to it when possible. +### struct TemporaryArray + +``TemporaryArray`` is a dynamically self-resizing array whose *initial* storage is **borrowed** -- most usefully a stack allocation vended by the ``withTemporaryArray(of:capacity:_:)`` function. As long as the element count stays within a seed buffer small enough to live on the stack, the array performs no heap allocation at all. The moment an insertion would exceed the borrowed capacity, the array transparently spills over into freshly allocated heap storage, moving its existing elements across, and from then on behaves like an ordinary heap-backed array that owns its storage. + +```swift + let sum = withTemporaryArray(of: Int.self, capacity: 64) { scratch in + for x in numbers where isHot(x) { + scratch.append(x * x) + } + var total = 0 + for i in scratch.indices { total += scratch[i] } + return total + } +``` + +Because it can hold a dependency on borrowed (stack) memory, ``TemporaryArray`` is *non-escapable*: instances cannot outlive the scope that provides their initial buffer. This is enforced by the compiler. To keep the accumulated elements past that scope, move them into an owning container with ``TemporaryArray/take()``, which hands back a ``UniqueArray`` -- transferring the heap buffer directly if the array had already spilled, or moving the elements into a fresh allocation otherwise. + +This makes ``TemporaryArray`` a good fit for algorithms that need scratch storage of an unknown final size where small cases dominate, such as collecting the results of mapping or filtering an arbitrary sequence: a reasonable lower-bound guess can be reserved on the stack, and only the unexpectedly large cases pay for a heap allocation. + +Its API deliberately mirrors ``UniqueArray`` (SE-0527), minus the operations that don't apply to a non-escapable scratch type, plus a few members unique to its borrow-then-spill design (``TemporaryArray/take()`` and the copying ``TemporaryArray/clone()`` operations). + ## Topics ### Types - ``UniqueArray`` - ``RigidArray`` +- ``TemporaryArray`` + +### Functions + +- ``withTemporaryArray(of:capacity:_:)`` diff --git a/Sources/BasicContainers/BasicContainers.docc/Extensions/TemporaryArray.md b/Sources/BasicContainers/BasicContainers.docc/Extensions/TemporaryArray.md new file mode 100644 index 000000000..bf7ac7c77 --- /dev/null +++ b/Sources/BasicContainers/BasicContainers.docc/Extensions/TemporaryArray.md @@ -0,0 +1,62 @@ +# ``BasicContainers/TemporaryArray`` + +## Topics + +### Creating a Temporary Array + +- ``withTemporaryArray(of:capacity:_:)`` +- ``init()`` +- ``init(capacity:)`` +- ``init(capacity:initializingWith:)`` + +### Inspecting a Temporary Array + +- ``isEmpty`` +- ``count`` +- ``capacity`` +- ``freeCapacity`` +- ``isTriviallyIdentical(to:)`` + +### Indices + +- ``Index`` +- ``startIndex`` +- ``endIndex`` +- ``indices`` + +### Accessing Elements + +- ``subscript(_:)`` +- ``swapAt(_:_:)`` +- ``edit(_:)`` + +### Memory Management + +- ``reallocate(capacity:)`` +- ``reserveCapacity(_:)`` + +### Moving and Copying Out + +- ``take()`` +- ``clone()`` +- ``clone(capacity:)`` + +### Spans + +- ``span`` +- ``mutableSpan`` +- ``nextSpan(after:maximumCount:)`` +- ``nextMutableSpan(after:maximumCount:)`` +- ``previousSpan(before:maximumCount:)`` + +### Appending Items + +- ``append(_:)`` +- ``append(addingCount:initializingWith:)`` +- ``append(repeating:count:)`` +- ``append(moving:)-(UnsafeMutableBufferPointer)`` +- ``append(moving:)-(OutputSpan)`` +- ``append(copying:)-(Sequence)`` +- ``append(copying:)-(Span)`` +- ``append(copying:)-(UnsafeBufferPointer)`` +- ``append(copying:)-(UnsafeMutableBufferPointer)`` diff --git a/Sources/BasicContainers/CMakeLists.txt b/Sources/BasicContainers/CMakeLists.txt index 29bc573c5..2f1781258 100644 --- a/Sources/BasicContainers/CMakeLists.txt +++ b/Sources/BasicContainers/CMakeLists.txt @@ -52,6 +52,13 @@ target_sources(${module_name} PRIVATE "UniqueArray/UniqueArray+Insertions.swift" "UniqueArray/UniqueArray+Removals.swift" "UniqueArray/UniqueArray+Replacements.swift" + "TemporaryArray/TemporaryArray.swift" + "TemporaryArray/TemporaryArray+Append.swift" + "TemporaryArray/TemporaryArray+Container.swift" + "TemporaryArray/TemporaryArray+Descriptions.swift" + "TemporaryArray/TemporaryArray+Equatable.swift" + "TemporaryArray/TemporaryArray+Hashable.swift" + "TemporaryArray/TemporaryArray+RangeReplaceable.swift" "HashTable/_HTable.swift" "HashTable/_HTable+Bitmap.swift" "HashTable/_HTable+Bucket.swift" diff --git a/Sources/BasicContainers/TemporaryArray/TemporaryArray+Append.swift b/Sources/BasicContainers/TemporaryArray/TemporaryArray+Append.swift new file mode 100644 index 000000000..2a0956aec --- /dev/null +++ b/Sources/BasicContainers/TemporaryArray/TemporaryArray+Append.swift @@ -0,0 +1,216 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import InternalCollectionsUtilities +import ContainersPreview +#endif + +#if compiler(>=6.3) + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// Adds an element to the end of the array, growing (and, if needed, spilling + /// to the heap) to make room. + /// + /// - Complexity: O(1), amortized over many invocations on the same array. + @_alwaysEmitIntoClient + public mutating func append(_ item: consuming Element) { + _ensureFreeCapacity(1) + unsafe _storage.initializeElement(at: _count, to: item) + _count &+= 1 + } + + /// Appends a given number of items to the end of the array by populating an + /// output span, growing (and, if needed, spilling to the heap) to make room. + /// + /// The closure may initialize fewer than `newItemCount` items; the array + /// gains exactly as many as the closure adds before it returns or throws. + /// + /// - Complexity: O(`newItemCount`), amortized. + @_alwaysEmitIntoClient + public mutating func append( + addingCount newItemCount: Int, + initializingWith initializer: (inout OutputSpan) throws(E) -> Void + ) throws(E) { + precondition(newItemCount >= 0, "Cannot add a negative number of items") + _ensureFreeCapacity(newItemCount) + let buffer = unsafe _freeSpace.extracting( + Range(uncheckedBounds: (0, newItemCount))) + var span = unsafe OutputSpan(buffer: buffer, initializedCount: 0) + defer { + _count &+= span.finalize(for: buffer) + span = OutputSpan() + } + return try initializer(&span) + } +} + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// Moves the elements of a buffer to the end of this array, leaving the + /// buffer uninitialized. + /// + /// - Complexity: O(`items.count`), amortized. + @_alwaysEmitIntoClient + public mutating func append( + moving items: UnsafeMutableBufferPointer + ) { + guard items.count > 0 else { return } + _ensureFreeCapacity(items.count) + let target = unsafe _freeSpace.extracting( + Range(uncheckedBounds: (0, items.count))) + let i = unsafe target.moveInitialize(fromContentsOf: items) + assert(i == items.count) + _count &+= items.count + } + + /// Moves the elements of an output span to the end of this array, leaving the + /// span empty. + /// + /// - Complexity: O(`items.count`), amortized. + @_alwaysEmitIntoClient + public mutating func append( + moving items: inout OutputSpan + ) { + items.withUnsafeMutableBufferPointer { buffer, count in + let source = unsafe buffer.extracting(Range(uncheckedBounds: (0, count))) + unsafe self.append(moving: source) + count = 0 + } + } +} + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray /*where Element: Copyable*/ { + /// Appends `count` copies of `repeatedValue` to the end of the array, growing + /// (and, if needed, spilling to the heap) to make room. + /// + /// This is `TemporaryArray`'s analogue of `UniqueArray`'s + /// `init(repeating:count:)`: it's an append rather than an initializer, + /// because a `TemporaryArray` is meant to be seeded (typically on the stack) + /// via `withTemporaryArray` and then filled. + /// + /// - Complexity: O(`count`), amortized. + @_alwaysEmitIntoClient + public mutating func append(repeating repeatedValue: Element, count: Int) { + precondition(count >= 0, "Cannot add a negative number of items") + guard count > 0 else { return } + _ensureFreeCapacity(count) + let target = unsafe _freeSpace.extracting( + Range(uncheckedBounds: (0, count))) + unsafe target.initialize(repeating: repeatedValue) + _count &+= count + } + + /// Copies the elements of a buffer to the end of this array. + /// + /// - Complexity: O(`newElements.count`), amortized. + @_alwaysEmitIntoClient + public mutating func append( + copying newElements: UnsafeBufferPointer + ) { + guard newElements.count > 0 else { return } + _ensureFreeCapacity(newElements.count) + let target = unsafe _freeSpace.extracting( + Range(uncheckedBounds: (0, newElements.count))) + _ = unsafe target.initialize(fromContentsOf: newElements) + _count &+= newElements.count + } + + /// Copies the elements of a buffer to the end of this array. + /// + /// - Complexity: O(`newElements.count`), amortized. + @_alwaysEmitIntoClient + public mutating func append( + copying items: UnsafeMutableBufferPointer + ) { + unsafe self.append(copying: UnsafeBufferPointer(items)) + } + + /// Copies the elements of a span to the end of this array. + /// + /// - Complexity: O(`newElements.count`), amortized. + @_alwaysEmitIntoClient + public mutating func append(copying newElements: Span) { + guard newElements.count > 0 else { return } + _ensureFreeCapacity(newElements.count) + let target = unsafe _freeSpace.extracting( + Range(uncheckedBounds: (0, newElements.count))) + unsafe newElements.withUnsafeBufferPointer { source in + _ = unsafe target.initialize(fromContentsOf: source) + } + _count &+= newElements.count + } + + /// Copies the elements of a sequence to the end of this array. + /// + /// If the sequence provides only a loose `underestimatedCount`, the array's + /// storage may need to be resized more than once (potentially spilling to the + /// heap along the way). + /// + /// - Complexity: O(*m*), where *m* is the length of `newElements`, amortized. + @_alwaysEmitIntoClient + public mutating func append(copying newElements: some Sequence) { + let done: Void? = newElements.withContiguousStorageIfAvailable { buffer in + _ensureFreeCapacity(buffer.count) + let target = unsafe _freeSpace.extracting( + Range(uncheckedBounds: (0, buffer.count))) + _ = unsafe target.initialize(fromContentsOf: buffer) + _count &+= buffer.count + return + } + if done != nil { return } + + _ensureFreeCapacity(newElements.underestimatedCount) + for item in newElements { + append(item) + } + } +} + +#if compiler(>=6.4) && UnstableContainersPreview +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// Appends all the items generated by a producer to the end of this array, + /// growing (and, if needed, spilling to the heap) as it goes. + /// + /// This is the building block for collecting the result of mapping, filtering + /// or otherwise transforming an arbitrary generative sequence whose final + /// length isn't known in advance. The producer's `underestimatedCount` is + /// used to size each bulk append. + /// + /// - Complexity: O(*n*) where *n* is the number of generated items, amortized. + @_alwaysEmitIntoClient + public mutating func append< + E: Error, + P: Producer & ~Copyable & ~Escapable + >( + from producer: inout P + ) throws(E) + where P.Element: ~Copyable { + var done = false + while !done { + let c = Swift.max(producer.underestimatedCount, 1) + _ensureFreeCapacity(c) + try self.append(addingCount: freeCapacity) { target throws(E) in + while !target.isFull, !done { + done = try !producer.generate(into: &target) + } + } + } + } +} +#endif + +#endif diff --git a/Sources/BasicContainers/TemporaryArray/TemporaryArray+Container.swift b/Sources/BasicContainers/TemporaryArray/TemporaryArray+Container.swift new file mode 100644 index 000000000..560d61011 --- /dev/null +++ b/Sources/BasicContainers/TemporaryArray/TemporaryArray+Container.swift @@ -0,0 +1,243 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import InternalCollectionsUtilities +import ContainersPreview +#endif + +#if compiler(>=6.3) + +#if compiler(>=6.4) && UnstableContainersPreview + +//MARK: - Protocol conformances + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray: Iterable_ where Element: ~Copyable { + public typealias IterableIterator_ = SpanIterator + + @_alwaysEmitIntoClient + @inline(__always) + public var underestimatedCount_: Int { count } + + @_alwaysEmitIntoClient + @inline(__always) + @_lifetime(borrow self) + public func makeIterableIterator_() -> IterableIterator_ { + SpanIterator(self.span) + } +} + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray: Container where Element: ~Copyable {} + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray: BidirectionalContainer where Element: ~Copyable {} + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray: RandomAccessContainer where Element: ~Copyable {} + +#endif // compiler(>=6.4) && UnstableContainersPreview + +//MARK: - Count + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// A Boolean value indicating whether the array is empty. + /// + /// - Complexity: O(1) + @_alwaysEmitIntoClient + @inline(__always) + public var isEmpty: Bool { _count == 0 } + + /// The number of elements in the array. + /// + /// - Complexity: O(1) + @_alwaysEmitIntoClient + @inline(__always) + public var count: Int { _count } +} + +//MARK: - Indices + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// A position in the array: an integer offset from the start. + public typealias Index = Int + + @_alwaysEmitIntoClient + @inline(__always) + public var startIndex: Int { 0 } + + @_alwaysEmitIntoClient + @inline(__always) + public var endIndex: Int { _count } + + @_alwaysEmitIntoClient + @inline(__always) + public var indices: Range { unsafe Range(uncheckedBounds: (0, _count)) } + + @_alwaysEmitIntoClient + @_transparent + internal func _checkItemIndex(_ index: Int) { + precondition( + UInt(bitPattern: index) < UInt(bitPattern: _count), + "Index out of bounds") + } + + @_alwaysEmitIntoClient + @_transparent + internal func _checkValidIndex(_ index: Int) { + precondition( + UInt(bitPattern: index) <= UInt(bitPattern: _count), + "Index out of bounds") + } + + @_alwaysEmitIntoClient + @_transparent + internal func _checkValidBounds(_ subrange: Range) { + precondition( + subrange.lowerBound >= 0 && subrange.upperBound <= _count, + "Index range out of bounds") + } +} + +//MARK: - Index navigation + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + @_alwaysEmitIntoClient + @inline(__always) + public func index(after index: Int) -> Int { index + 1 } + + @_alwaysEmitIntoClient + @inline(__always) + public func index(before index: Int) -> Int { index - 1 } + + @_alwaysEmitIntoClient + @inline(__always) + public func formIndex(after index: inout Int) { index += 1 } + + @_alwaysEmitIntoClient + @inline(__always) + public func formIndex(before index: inout Int) { index -= 1 } + + @_alwaysEmitIntoClient + @inline(__always) + public func index(_ index: Int, offsetBy n: Int) -> Int { index + n } + + @_alwaysEmitIntoClient + @inline(__always) + public func distance(from start: Int, to end: Int) -> Int { end - start } + + @_alwaysEmitIntoClient + public func formIndex( + _ index: inout Int, offsetBy n: inout Int, limitedBy limit: Int + ) { + index._advance(by: &n, limitedBy: limit) + } +} + +//MARK: - Element access + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + @_alwaysEmitIntoClient + @inline(__always) + internal func _ptr(to index: Int) -> UnsafePointer { + _checkItemIndex(index) + let p = unsafe _storage.baseAddress.unsafelyUnwrapped.advanced(by: index) + return unsafe UnsafePointer(p) + } + + @_alwaysEmitIntoClient + @inline(__always) + internal mutating func _mutablePtr( + to index: Int + ) -> UnsafeMutablePointer { + _checkItemIndex(index) + return unsafe _storage.baseAddress.unsafelyUnwrapped.advanced(by: index) + } + + /// Accesses the element at the specified position. + /// + /// - Complexity: O(1) + @_alwaysEmitIntoClient + public subscript(position: Int) -> Element { + @inline(__always) + unsafeAddress { + unsafe _ptr(to: position) + } + @inline(__always) + unsafeMutableAddress { + unsafe _mutablePtr(to: position) + } + } + + /// Exchanges the values at the specified indices of the array. + /// + /// - Complexity: O(1) + @_alwaysEmitIntoClient + public mutating func swapAt(_ i: Int, _ j: Int) { + _checkItemIndex(i) + _checkItemIndex(j) + unsafe _items.swapAt(i, j) + } +} + +//MARK: - Bulk access + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// Return a span over the contiguous storage chunk starting at `index`, of at + /// most `maximumCount` items, advancing `index` past it. + /// + /// - Complexity: O(1) + @_alwaysEmitIntoClient + @_lifetime(borrow self) + public func nextSpan( + after index: inout Int, maximumCount: Int + ) -> Span { + _checkValidIndex(index) + precondition(maximumCount > 0, "maximumCount must be positive") + let start = index + index = start &+ Swift.min(maximumCount, _count &- start) + return _span(in: Range(uncheckedBounds: (start, index))) + } + + @_alwaysEmitIntoClient + @_lifetime(&self) + public mutating func nextMutableSpan( + after index: inout Int, maximumCount: Int + ) -> MutableSpan { + _checkValidIndex(index) + precondition(maximumCount > 0, "maximumCount must be positive") + let start = index + index = start &+ Swift.min(maximumCount, _count &- start) + return _mutableSpan(in: Range(uncheckedBounds: (start, index))) + } + + @_alwaysEmitIntoClient + @_lifetime(borrow self) + public func previousSpan( + before index: inout Int, maximumCount: Int + ) -> Span { + _checkValidIndex(index) + precondition(maximumCount > 0, "maximumCount must be positive") + let start = index + index = start &- Swift.min(maximumCount, start) + return _span(in: Range(uncheckedBounds: (index, start))) + } +} + +#endif diff --git a/Sources/BasicContainers/TemporaryArray/TemporaryArray+Descriptions.swift b/Sources/BasicContainers/TemporaryArray/TemporaryArray+Descriptions.swift new file mode 100644 index 000000000..b71ead2c1 --- /dev/null +++ b/Sources/BasicContainers/TemporaryArray/TemporaryArray+Descriptions.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.3) + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray /*: CustomStringConvertible FIXME: conform once the protocol supports ~Copyable & ~Escapable types */ +where Element: ~Copyable { + @_alwaysEmitIntoClient + public var description: String { + // FIXME: Print the item descriptions when available. + "<\(count) items>" + } +} + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray /*: CustomDebugStringConvertible FIXME: conform once the protocol supports ~Copyable & ~Escapable types */ +where Element: ~Copyable { + @_alwaysEmitIntoClient + public var debugDescription: String { + // FIXME: Print the item descriptions when available. + "<\(count) items>" + } +} + +#endif diff --git a/Sources/BasicContainers/TemporaryArray/TemporaryArray+Equatable.swift b/Sources/BasicContainers/TemporaryArray/TemporaryArray+Equatable.swift new file mode 100644 index 000000000..ca7c0cb02 --- /dev/null +++ b/Sources/BasicContainers/TemporaryArray/TemporaryArray+Equatable.swift @@ -0,0 +1,45 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import InternalCollectionsUtilities +import ContainersPreview +#endif + +#if compiler(>=6.3) + +#if compiler(>=6.4) && UnstableContainersPreview +@available(SwiftStdlib 5.0, *) +extension TemporaryArray: Equatable where Element: Equatable & ~Copyable { + @_alwaysEmitIntoClient + public static func ==( + left: borrowing Self, + right: borrowing Self + ) -> Bool { + left.span._elementsEqual(to: right.span) + } +} +#else +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: Equatable { + @_alwaysEmitIntoClient + public static func ==( + left: borrowing Self, + right: borrowing Self + ) -> Bool { + left.span._elementsEqual(to: right.span) + } +} +#endif + +#endif diff --git a/Sources/BasicContainers/TemporaryArray/TemporaryArray+Hashable.swift b/Sources/BasicContainers/TemporaryArray/TemporaryArray+Hashable.swift new file mode 100644 index 000000000..6d1a31a64 --- /dev/null +++ b/Sources/BasicContainers/TemporaryArray/TemporaryArray+Hashable.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +#if compiler(>=6.3) + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray /*: Hashable FIXME: conform once Hashable supports ~Copyable & ~Escapable types */ +where Element: Hashable /* & ~Copyable */ { + @_alwaysEmitIntoClient + public func hash(into hasher: inout Hasher) { + hasher.combine(self.count) + let span = self.span + for i in 0 ..< count { + hasher.combine(span[unchecked: i]) + } + } +} + +#endif diff --git a/Sources/BasicContainers/TemporaryArray/TemporaryArray+RangeReplaceable.swift b/Sources/BasicContainers/TemporaryArray/TemporaryArray+RangeReplaceable.swift new file mode 100644 index 000000000..83c491a7b --- /dev/null +++ b/Sources/BasicContainers/TemporaryArray/TemporaryArray+RangeReplaceable.swift @@ -0,0 +1,484 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import InternalCollectionsUtilities +import ContainersPreview +#endif + +#if compiler(>=6.4) && UnstableContainersPreview + +// Note on protocol conformances +// ============================= +// +// `TemporaryArray` is non-escapable (it can hold a dependency on borrowed stack +// memory), and that currently rules out conforming to two protocols it would +// otherwise be a natural fit for: +// +// * `DynamicContainer` is declared `~Copyable` but *not* `~Escapable`, so it +// requires escapable conformers. (Its `init()` / `init(minimumCapacity:)` +// requirements presuppose a self-owning, escapable container.) +// +// * `RangeReplaceableContainer` *is* `~Escapable`-tolerant, but its +// `SubrangeConsumer` (a `Drain`) needs to hold a mutable back-reference to +// the array so it can close the gap when destroyed. The tool for that, +// `MutableRef`, still requires its pointee to be escapable (there is a +// `// FIXME: ~Escapable` on its declaration). Until that is generalized, a +// non-escapable container cannot vend such a consumer. +// +// Rather than conform, `TemporaryArray` therefore offers the same operations +// directly: `replace(...)`, a closure-based `consume(_:consumingWith:)` (which +// needs no stored back-reference), and the usual removal helpers below. The +// read-only `Container` / `BidirectionalContainer` / `RandomAccessContainer` +// conformances are unaffected. + +//MARK: - Gap management + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + @_alwaysEmitIntoClient + internal mutating func _closeGap(at index: Int, count: Int) { + guard count > 0 else { return } + let source = unsafe _storage.extracting( + Range(uncheckedBounds: (index + count, _count))) + let target = unsafe _storage.extracting( + Range(uncheckedBounds: (index, index + source.count))) + let i = unsafe target.moveInitialize(fromContentsOf: source) + assert(i == target.endIndex) + } + + @_alwaysEmitIntoClient + @unsafe + internal mutating func _openGap( + at index: Int, count: Int + ) -> UnsafeMutableBufferPointer { + assert(index >= 0 && index <= _count) + assert(count <= freeCapacity) + guard count > 0 else { + return unsafe _storage.extracting(Range(uncheckedBounds: (index, index))) + } + let source = unsafe _storage.extracting( + Range(uncheckedBounds: (index, _count))) + let target = unsafe _storage.extracting( + Range(uncheckedBounds: (index + count, _count + count))) + let i = unsafe target.moveInitialize(fromContentsOf: source) + assert(i == target.count) + return unsafe _storage.extracting( + Range(uncheckedBounds: (index, index + count))) + } + + /// Resize the gap in `subrange` to hold `newItemCount` items, moving trailing + /// elements as needed and adjusting `count`. Returns the (uninitialized) gap. + @_alwaysEmitIntoClient + @unsafe + internal mutating func _resizeGap( + in subrange: Range, to newItemCount: Int + ) -> UnsafeMutableBufferPointer { + assert(subrange.lowerBound >= 0 && subrange.upperBound <= _count) + assert(newItemCount >= 0 && newItemCount - subrange.count <= freeCapacity) + if newItemCount > subrange.count { + _ = unsafe _openGap( + at: subrange.upperBound, count: newItemCount - subrange.count) + } else if newItemCount < subrange.count { + _closeGap( + at: subrange.lowerBound + newItemCount, + count: subrange.count - newItemCount) + } + _count += newItemCount - subrange.count + let gapRange = unsafe Range( + uncheckedBounds: (subrange.lowerBound, subrange.lowerBound + newItemCount)) + return unsafe _storage.extracting(gapRange) + } +} + +//MARK: - Replacing + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// Replaces the elements in `subrange` with `newItemCount` new items, + /// consuming the removed items in place through an input span and + /// initializing the replacements through an output span. The array grows + /// (and spills to the heap if needed) to accommodate a net increase in count. + /// + /// - Complexity: O(`count` + `newItemCount`), amortized. + @_alwaysEmitIntoClient + public mutating func replace( + removing subrange: Range, + consumingWith consumer: (inout InputSpan) -> Void, + addingCount newItemCount: Int, + initializingWith initializer: (inout OutputSpan) throws(E) -> Void + ) throws(E) { + _checkValidBounds(subrange) + precondition(newItemCount >= 0, "Cannot add a negative number of items") + let netIncrease = newItemCount - subrange.count + if netIncrease > 0 { + _ensureFreeCapacity(netIncrease) + } + do { + // Consume the items to be removed. + let buffer = unsafe _storage.extracting(subrange) + var span = unsafe InputSpan(buffer: buffer, initializedCount: buffer.count) + consumer(&span) + _ = consume span + } + do { + // Open a gap and let the caller initialize the replacements. + let target = unsafe _resizeGap(in: subrange, to: newItemCount) + var span = unsafe OutputSpan(buffer: target, initializedCount: 0) + defer { + let c = span.finalize(for: target) + if c < newItemCount { + self._closeGap( + at: subrange.lowerBound &+ c, count: newItemCount &- c) + _count &-= newItemCount &- c + } + span = OutputSpan() + } + try initializer(&span) + } + } + + /// Replaces the elements in `subrange` with `newItemCount` new items + /// initialized through an output span, destroying the removed items. + /// + /// - Complexity: O(`count` + `newItemCount`), amortized. + @_alwaysEmitIntoClient + public mutating func replaceSubrange( + _ subrange: Range, + addingCount newItemCount: Int, + initializingWith initializer: (inout OutputSpan) throws(E) -> Void + ) throws(E) { + try replace( + removing: subrange, + consumingWith: { _ in }, + addingCount: newItemCount, + initializingWith: initializer) + } + + /// Replaces the elements in `subrange` by moving the elements of a fully + /// initialized buffer into their place. On return, the buffer is left + /// uninitialized. The array grows (and spills to the heap if needed) to + /// accommodate a net increase in count. + /// + /// - Complexity: O(`count` + `newElements.count`), amortized. + @_alwaysEmitIntoClient + public mutating func replaceSubrange( + _ subrange: Range, + moving newElements: UnsafeMutableBufferPointer + ) { + replaceSubrange(subrange, addingCount: newElements.count) { target in + target._append(moving: newElements) + } + } + + /// Replaces the elements in `subrange` by moving the contents of an output + /// span into their place. On return, the span is left empty. The array grows + /// (and spills to the heap if needed) to accommodate a net increase in count. + /// + /// - Complexity: O(`count` + `items.count`), amortized. + @_alwaysEmitIntoClient + public mutating func replaceSubrange( + _ subrange: Range, + moving items: inout OutputSpan + ) { + replaceSubrange(subrange, addingCount: items.count) { target in + target._append(moving: &items) + } + } +} + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray /*where Element: Copyable*/ { + /// Replaces the elements in `subrange` by copying the elements of a fully + /// initialized buffer into their place. The array grows (and spills to the + /// heap if needed) to accommodate a net increase in count. + /// + /// - Complexity: O(`count` + `newElements.count`), amortized. + @_alwaysEmitIntoClient + public mutating func replaceSubrange( + _ subrange: Range, + copying newElements: UnsafeBufferPointer + ) { + replaceSubrange(subrange, addingCount: newElements.count) { target in + target._append(copying: newElements) + } + } + + /// Replaces the elements in `subrange` by copying the elements of a fully + /// initialized buffer into their place. + /// + /// - Complexity: O(`count` + `newElements.count`), amortized. + @_alwaysEmitIntoClient + public mutating func replaceSubrange( + _ subrange: Range, + copying newElements: UnsafeMutableBufferPointer + ) { + unsafe replaceSubrange(subrange, copying: UnsafeBufferPointer(newElements)) + } + + /// Replaces the elements in `subrange` by copying the elements of a span into + /// their place. + /// + /// - Complexity: O(`count` + `newElements.count`), amortized. + @_alwaysEmitIntoClient + public mutating func replaceSubrange( + _ subrange: Range, + copying newElements: Span + ) { + replaceSubrange(subrange, addingCount: newElements.count) { target in + target._append(copying: newElements) + } + } + + /// Replaces the elements in `subrange` by copying the elements of a + /// collection into their place. + /// + /// - Complexity: O(`count` + `newElements.count`), amortized. + @_alwaysEmitIntoClient + public mutating func replaceSubrange( + _ subrange: Range, + copying newElements: some Collection + ) { + let newItemCount = newElements.count + replaceSubrange(subrange, addingCount: newItemCount) { target in + let done: Void? = newElements.withContiguousStorageIfAvailable { buffer in + target._append(copying: buffer) + } + if done != nil { return } + for item in newElements { target.append(item) } + } + } +} + +//MARK: - Inserting + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// Inserts `newItemCount` items at `index`, initialized through an output + /// span, growing (and spilling to the heap if needed) to make room. + /// + /// - Complexity: O(`count` + `newItemCount`), amortized. + @_alwaysEmitIntoClient + public mutating func insert( + addingCount newItemCount: Int, + at index: Int, + initializingWith initializer: (inout OutputSpan) throws(E) -> Void + ) throws(E) { + try replaceSubrange( + Range(uncheckedBounds: (index, index)), + addingCount: newItemCount, + initializingWith: initializer) + } + + /// Inserts a single element at `index`, growing as needed. + /// + /// - Complexity: O(`count`), amortized. + @_alwaysEmitIntoClient + public mutating func insert(_ item: consuming Element, at index: Int) { + var item: Element? = item + insert(addingCount: 1, at: index) { target in + target.append(item.take()!) + } + } + + /// Moves the elements of a fully initialized buffer into this array at + /// `index`, leaving the buffer uninitialized. + /// + /// - Complexity: O(`count` + `items.count`), amortized. + @_alwaysEmitIntoClient + public mutating func insert( + moving items: UnsafeMutableBufferPointer, + at index: Int + ) { + insert(addingCount: items.count, at: index) { target in + target._append(moving: items) + } + } + + /// Moves the elements of an output span into this array at `index`, leaving + /// the span empty. + /// + /// - Complexity: O(`count` + `items.count`), amortized. + @_alwaysEmitIntoClient + public mutating func insert( + moving items: inout OutputSpan, + at index: Int + ) { + insert(addingCount: items.count, at: index) { target in + target._append(moving: &items) + } + } +} + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray /*where Element: Copyable*/ { + /// Copies the elements of a fully initialized buffer into this array at + /// `index`. + /// + /// - Complexity: O(`count` + `newElements.count`), amortized. + @_alwaysEmitIntoClient + public mutating func insert( + copying newElements: UnsafeBufferPointer, at index: Int + ) { + insert(addingCount: newElements.count, at: index) { target in + target._append(copying: newElements) + } + } + + /// Copies the elements of a fully initialized buffer into this array at + /// `index`. + /// + /// - Complexity: O(`count` + `newElements.count`), amortized. + @_alwaysEmitIntoClient + public mutating func insert( + copying newElements: UnsafeMutableBufferPointer, at index: Int + ) { + unsafe self.insert(copying: UnsafeBufferPointer(newElements), at: index) + } + + /// Copies the elements of a span into this array at `index`. + /// + /// - Complexity: O(`count` + `newElements.count`), amortized. + @_alwaysEmitIntoClient + public mutating func insert( + copying newElements: Span, at index: Int + ) { + insert(addingCount: newElements.count, at: index) { target in + target._append(copying: newElements) + } + } + + /// Copies the elements of a collection into this array at `index`. + /// + /// - Complexity: O(`count` + `newElements.count`), amortized. + @_alwaysEmitIntoClient + public mutating func insert( + copying newElements: some Collection, at index: Int + ) { + insert(addingCount: newElements.count, at: index) { target in + let done: Void? = newElements.withContiguousStorageIfAvailable { buffer in + target._append(copying: buffer) + } + if done != nil { return } + for item in newElements { target.append(item) } + } + } +} + +//MARK: - Removing + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// Removes the elements in `subrange`, passing an input span to `consumer` so + /// they can be consumed in place. Any items the consumer leaves behind are + /// destroyed. This needs no stored back-reference, so unlike a `Drain`-based + /// consumer it works on this non-escapable type. + /// + /// - Complexity: O(`count`) + @_alwaysEmitIntoClient + public mutating func consume( + _ subrange: Range, + consumingWith consumer: (inout InputSpan) -> Void + ) { + _checkValidBounds(subrange) + guard !subrange.isEmpty else { + var span = InputSpan() + consumer(&span) + return + } + let buffer = unsafe _storage.extracting(subrange) + var span = unsafe InputSpan(buffer: buffer, initializedCount: buffer.count) + consumer(&span) + _ = consume span + _closeGap(at: subrange.lowerBound, count: subrange.count) + _count -= subrange.count + } + + /// Removes and destroys the elements in `subrange`. + /// + /// - Complexity: O(`count`) + @_alwaysEmitIntoClient + public mutating func removeSubrange(_ subrange: Range) { + consume(subrange) { _ in } + } + + /// Removes and destroys the elements in `subrange`. + /// + /// - Complexity: O(`count`) + @_alwaysEmitIntoClient + public mutating func removeSubrange(_ subrange: some RangeExpression) { + removeSubrange(subrange.relative(to: indices)) + } + + /// Removes and returns the element at the specified position, closing the gap. + /// + /// - Complexity: O(`count`) + @_alwaysEmitIntoClient + @discardableResult + public mutating func remove(at index: Int) -> Element { + _checkItemIndex(index) + let old = unsafe _storage.moveElement(from: index) + _closeGap(at: index, count: 1) + _count &-= 1 + return old + } + + /// Removes and destroys the last `k` elements of the array. + /// + /// - Complexity: O(`k`) + @_alwaysEmitIntoClient + public mutating func removeLast(_ k: Int) { + if k == 0 { return } + precondition( + k >= 0 && k <= _count, + "Count of elements to remove is out of bounds") + unsafe _storage.extracting( + Range(uncheckedBounds: (_count &- k, _count)) + ).deinitialize() + _count &-= k + } + + /// Removes and destroys all elements, optionally keeping the current storage. + /// + /// - Complexity: O(`count`) + @_alwaysEmitIntoClient + public mutating func removeAll(keepingCapacity keepCapacity: Bool = false) { + unsafe _items.deinitialize() + _count = 0 + if !keepCapacity, _ownsStorage { + unsafe _storage.deallocate() + unsafe _storage = .init(start: nil, count: 0) + } + } + + /// Removes and returns the last element. + /// + /// - Complexity: O(1) + @_alwaysEmitIntoClient + public mutating func removeLast() -> Element { + precondition(_count > 0, "Cannot remove last element from an empty array") + _count &-= 1 + return unsafe _storage.moveElement(from: _count) + } + + /// Removes and returns the last element, or returns `nil` if empty. + /// + /// - Complexity: O(1) + @_alwaysEmitIntoClient + public mutating func popLast() -> Element? { + guard _count > 0 else { return nil } + return removeLast() + } +} + +#endif diff --git a/Sources/BasicContainers/TemporaryArray/TemporaryArray.swift b/Sources/BasicContainers/TemporaryArray/TemporaryArray.swift new file mode 100644 index 000000000..82511ef18 --- /dev/null +++ b/Sources/BasicContainers/TemporaryArray/TemporaryArray.swift @@ -0,0 +1,475 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +#if !COLLECTIONS_SINGLE_MODULE +import InternalCollectionsUtilities +import ContainersPreview +#endif + +#if compiler(>=6.3) + +/// A dynamically self-resizing, noncopyable array of potentially noncopyable +/// elements whose initial storage is *borrowed* from the caller, but that +/// transparently spills over into freshly allocated heap storage once it grows +/// beyond the capacity of that initial buffer. +/// +/// `TemporaryArray` is designed to be seeded with a small buffer, most usefully +/// a *stack* allocation vended by +/// ``withTemporaryArray(of:capacity:_:)``. As long as the number +/// of elements stays within the reserved capacity, the array operates entirely +/// out of that borrowed buffer, incurring no heap traffic at all. The moment an +/// insertion would exceed the borrowed capacity, the array allocates a heap +/// buffer (using the same geometric growth curve as ``UniqueArray``), moves its +/// existing elements over, and from then on behaves like an ordinary +/// dynamically-resizing array that owns its storage. +/// +/// This makes `TemporaryArray` a good fit for algorithms that need scratch +/// storage of an unknown final size where small cases dominate, such as +/// collecting the results of mapping/filtering an arbitrary sequence: a good +/// lower-bound guess (e.g. `underestimatedCount`) can be reserved on the stack, +/// and only the unexpectedly large cases pay for a heap allocation. +/// +/// Because it can hold a dependency on borrowed (stack) memory, `TemporaryArray` +/// is a non-escapable type: instances cannot outlive the scope that provides +/// their initial buffer. This is enforced by the compiler. To extract the +/// contents past that scope, move them into an owning container such as +/// ``UniqueArray`` (see ``take()``). +@available(SwiftStdlib 5.0, *) +@safe +@frozen +public struct TemporaryArray: ~Copyable, ~Escapable { + /// The currently active storage buffer. This is either the borrowed buffer + /// the array was seeded with (when `_ownsStorage` is false), or a heap buffer + /// this array allocated and is responsible for freeing (when `_ownsStorage` + /// is true). + @usableFromInline + internal var _storage: UnsafeMutableBufferPointer + + /// The number of initialized elements at the start of `_storage`. + @usableFromInline + internal var _count: Int + + /// Whether this array owns (and must therefore deallocate) `_storage`. + /// + /// This starts out false when the array is seeded with a borrowed buffer, and + /// flips to true the first time the array spills its contents into a heap + /// allocation. Once true, it stays true. + @usableFromInline + internal var _ownsStorage: Bool + + @_alwaysEmitIntoClient + deinit { + unsafe _storage.extracting(0 ..< _count).deinitialize() + if _ownsStorage { + unsafe _storage.deallocate() + } + } + + /// Creates an array that borrows the given buffer as its initial storage. + /// + /// The buffer's memory must remain valid throughout the lifetime of the + /// resulting array; this is what makes `TemporaryArray` non-escapable. The + /// buffer is assumed to be entirely uninitialized; the new array starts + /// empty. + /// + /// This is currently internal: `TemporaryArray` is only vended through + /// ``withTemporaryArray(of:capacity:_:)``. It can be exposed as public API + /// later if a use case for a caller-provided seed buffer arises. + @unsafe + @_alwaysEmitIntoClient + @_lifetime(borrow buffer) + internal init(borrow buffer: UnsafeMutableBufferPointer) { + unsafe _storage = buffer + _count = 0 + _ownsStorage = false + } +} + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray: @unchecked Sendable +where Element: Sendable & ~Copyable {} + +//MARK: - Heap-only construction + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// Creates an empty array that owns a freshly allocated heap buffer with the + /// given capacity. + /// + /// Unlike instances seeded through + /// ``withTemporaryArray(of:capacity:_:)``, an array created this + /// way never borrows any external memory, so it carries an immortal lifetime + /// and may escape freely. This initializer exists primarily so that + /// `TemporaryArray` can satisfy the requirements of dynamic container + /// protocols; for purely heap-backed storage, prefer ``UniqueArray``. + @_alwaysEmitIntoClient + @_lifetime(immortal) + public init(capacity: Int) { + precondition(capacity >= 0, "Array capacity must be nonnegative") + if capacity > 0 { + unsafe _storage = .allocate(capacity: capacity) + } else { + unsafe _storage = .init(start: nil, count: 0) + } + _count = 0 + _ownsStorage = true + } + + /// Creates an empty array that owns no storage. + @_alwaysEmitIntoClient + @_lifetime(immortal) + public init() { + self.init(capacity: 0) + } +} + +//MARK: - Heap-only construction with an initializer + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// Creates a new heap-backed array with the specified capacity, directly + /// initializing its storage using an output span. + /// + /// Like ``init(capacity:)``, an array created this way never borrows any + /// external memory, so it carries an immortal lifetime and may escape freely. + /// + /// - Parameters: + /// - capacity: The storage capacity of the new array. + /// - initializer: A callback that gets called at most once to directly + /// populate newly reserved storage within the array. The function + /// is allowed to add fewer than `capacity` items. The array is + /// initialized with however many items the callback adds to the + /// output span before it returns (or before it throws an error). + @_alwaysEmitIntoClient + @_lifetime(immortal) + public init( + capacity: Int, + initializingWith initializer: (inout OutputSpan) throws(E) -> Void + ) throws(E) { + self.init(capacity: capacity) + try edit(initializer) + } +} + +//MARK: - Basics + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// The number of elements the array can currently hold without reallocating + /// (or spilling to the heap). + /// + /// - Complexity: O(1) + @_alwaysEmitIntoClient + @_transparent + public var capacity: Int { _assumeNonNegative(unsafe _storage.count) } + + /// The number of additional elements that can be added without reallocating. + /// + /// - Complexity: O(1) + @_alwaysEmitIntoClient + @_transparent + public var freeCapacity: Int { _assumeNonNegative(capacity &- _count) } + + /// Returns a Boolean value indicating whether two arrays are backed by the + /// same storage, at the same count. + /// + /// This is a lightweight identity check; it does not compare elements. Two + /// arrays with equal contents in distinct storage are *not* trivially + /// identical. + /// + /// - Complexity: O(1) + @_alwaysEmitIntoClient + public func isTriviallyIdentical(to other: borrowing Self) -> Bool { + unsafe _storage.baseAddress == other._storage.baseAddress + && _count == other._count + } +} + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + @_alwaysEmitIntoClient + internal var _items: UnsafeMutableBufferPointer { + unsafe _storage.extracting(Range(uncheckedBounds: (0, _count))) + } + + @_alwaysEmitIntoClient + internal var _freeSpace: UnsafeMutableBufferPointer { + unsafe _storage.extracting(Range(uncheckedBounds: (_count, capacity))) + } +} + +//MARK: - Span creation + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// A span over the elements of this array, providing direct read-only access. + /// + /// - Complexity: O(1) + public var span: Span { + @_lifetime(borrow self) + @_alwaysEmitIntoClient + get { + let result = unsafe Span(_unsafeElements: _items) + return unsafe _overrideLifetime(result, borrowing: self) + } + } + + /// A mutable span over the elements of this array, providing direct mutating + /// access. + /// + /// - Complexity: O(1) + public var mutableSpan: MutableSpan { + @_lifetime(&self) + @_alwaysEmitIntoClient + mutating get { + let result = unsafe MutableSpan(_unsafeElements: _items) + return unsafe _overrideLifetime(result, mutating: &self) + } + } + + @_alwaysEmitIntoClient + @_lifetime(borrow self) + internal func _span(in range: Range) -> Span { + span.extracting(range) + } + + @_alwaysEmitIntoClient + @_lifetime(&self) + internal mutating func _mutableSpan( + in range: Range + ) -> MutableSpan { + let result = unsafe MutableSpan(_unsafeElements: _items.extracting(range)) + return unsafe _overrideLifetime(result, mutating: &self) + } +} + +//MARK: - Editing + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// Arbitrarily edit the array's current storage by invoking a user-supplied + /// closure with a mutable `OutputSpan` view over it. + /// + /// The closure may add, remove or reorder items; it must not change the + /// span's capacity. (This operation does not resize the array's storage; to + /// guarantee free capacity beforehand, call ``reserveCapacity(_:)``.) + /// + /// - Complexity: Adds O(1) overhead to the complexity of the closure. + @_alwaysEmitIntoClient + public mutating func edit( + _ body: (inout OutputSpan) throws(E) -> R + ) throws(E) -> R { + var span = unsafe OutputSpan(buffer: _storage, initializedCount: _count) + defer { + _count = span.finalize(for: _storage) + span = OutputSpan() + } + return try body(&span) + } +} + +//MARK: - Resizing & spilling + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// Replace the array's storage buffer with a freshly allocated heap buffer of + /// the given capacity, moving all existing elements over. + /// + /// If the array was still using its borrowed (stack) seed buffer, this is the + /// point at which it "spills" to the heap: the borrowed buffer is left + /// untouched (the caller still owns it) and the array takes ownership of the + /// new heap buffer. Otherwise, the old heap buffer is deallocated as usual. + /// + /// - Parameter newCapacity: The desired new capacity. Must be `>= count`. + /// + /// - Complexity: O(`count`) + @_alwaysEmitIntoClient + public mutating func reallocate(capacity newCapacity: Int) { + precondition(newCapacity >= _count, "TemporaryArray capacity overflow") + guard newCapacity != capacity || !_ownsStorage else { return } + let newStorage: UnsafeMutableBufferPointer = .allocate( + capacity: newCapacity) + let i = unsafe newStorage.moveInitialize(fromContentsOf: _items) + assert(i == _count) + // Only free the old buffer if we owned it. A borrowed seed buffer is left + // for its owner (e.g. `withTemporaryArray`) to clean up. + if _ownsStorage { + unsafe _storage.deallocate() + } + unsafe _storage = newStorage + _ownsStorage = true + } + + /// Ensure that the array can hold at least `n` elements without reallocating, + /// growing (and, if necessary, spilling to the heap) if it cannot. + /// + /// - Complexity: O(`count`) if a reallocation is triggered, O(1) otherwise. + @_alwaysEmitIntoClient + public mutating func reserveCapacity(_ n: Int) { + guard capacity < n else { return } + reallocate(capacity: n) + } + + @_alwaysEmitIntoClient + @_transparent + internal func _grow(freeCapacity: Int) -> Int { + Swift.max(_count &+ freeCapacity, _growUniqueArrayCapacity(capacity)) + } + + @_alwaysEmitIntoClient + @_transparent + internal mutating func _ensureFreeCapacity(_ freeCapacity: Int) { + guard self.freeCapacity < freeCapacity else { return } + reallocate(capacity: _grow(freeCapacity: freeCapacity)) + } +} + +//MARK: - Moving out + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray where Element: ~Copyable { + /// Move the contents of this array into a newly created ``UniqueArray``, + /// leaving this array empty. + /// + /// Use this to hand the accumulated elements back out of the scope that owns + /// the array's seed buffer: the result is an ordinary heap-backed, + /// escapable container. If the array had already spilled to the heap, its + /// existing storage is transferred without copying; otherwise the elements + /// are moved out of the borrowed buffer into a fresh allocation. + /// + /// - Complexity: O(1) if the array has already spilled to the heap; + /// O(`count`) otherwise. + @_alwaysEmitIntoClient + public mutating func take() -> UniqueArray { + if _ownsStorage { + // Transfer ownership of the heap buffer directly. + let storage = unsafe RigidArray( + _storage: _storage, count: _count) + unsafe _storage = .init(start: nil, count: 0) + _count = 0 + _ownsStorage = true + return UniqueArray(_storage: storage) + } + // Still borrowing: move elements into a fresh heap allocation. + var result = UniqueArray(minimumCapacity: _count) + result._storage.append(moving: _items) + _count = 0 + return result + } +} + +//MARK: - Copying + +@available(SwiftStdlib 5.0, *) +extension TemporaryArray /*where Element: Copyable*/ { + /// Copies the contents of this array into a newly allocated, heap-backed array + /// with just enough capacity to hold all its elements. + /// + /// The result owns its storage and carries an immortal lifetime (it does not + /// borrow this array's storage), so it can escape freely. Combine with + /// ``take()`` to lift the contents into an owning ``UniqueArray``. + /// + /// - Complexity: O(`count`) + @_alwaysEmitIntoClient + @_lifetime(immortal) + public func clone() -> Self { + clone(capacity: count) + } + + /// Copies the contents of this array into a newly allocated, heap-backed array + /// with the specified capacity. + /// + /// - Parameter capacity: The desired capacity of the result. Must be `>= + /// count`. + /// + /// - Complexity: O(`count`) + @_alwaysEmitIntoClient + @_lifetime(immortal) + public func clone(capacity: Int) -> Self { + precondition(capacity >= count, "TemporaryArray capacity overflow") + var result = TemporaryArray(capacity: capacity) + result.append(copying: span) + return result + } +} + +//MARK: - Scoped construction over a stack allocation + +/// The maximum size, in bytes, of the initial buffer that +/// ``withTemporaryArray(of:capacity:_:)`` will place on the stack. +/// +/// This mirrors the threshold `withUnsafeTemporaryAllocation` uses internally: +/// requests at or below this size are served from the stack, larger requests +/// are heap allocated. We hardcode it so that an oversized initial capacity +/// skips the stack path entirely rather than relying on the standard library +/// to silently fall back to the heap. That allows `take()` to take ownership +/// off the heap allocation instead of allocating and moving the elements over. +@_alwaysEmitIntoClient +@_transparent +internal var _temporaryArrayStackByteLimit: Int { 1024 } + +/// Provides a dynamically-resizing array that is initially backed by a stack +/// allocation of the requested capacity, spilling over to the heap only if it +/// grows beyond it. +/// +/// This is the primary way to create a ``TemporaryArray``. The array passed to +/// `body` starts empty with room for `capacity` elements. As long as the +/// array's element count stays at or below that capacity, no heap allocation +/// occurs, provided the requested storage fits within the stack budget. +/// +/// To keep latency predictable, the initial buffer is placed on the stack only +/// if it occupies at most `_temporaryArrayStackByteLimit` (1024) bytes; a larger +/// initial `capacity` is heap allocated up front instead. Either way the array +/// grows on the heap once it exceeds its initial capacity. +/// +/// The array cannot escape `body` (it is non-escapable). To keep its contents, +/// move them into an owning container with ``TemporaryArray/take()``: +/// +/// let evens: UniqueArray = withTemporaryArray( +/// of: Int.self, capacity: source.underestimatedCount +/// ) { scratch in +/// for x in source where x.isMultiple(of: 2) { scratch.append(x) } +/// return scratch.take() +/// } +/// +/// - Parameters: +/// - type: The element type of the array. +/// - capacity: The number of elements to reserve up front. +/// - body: A closure that receives the freshly created, empty array. +/// - Returns: The result of `body`. +@available(SwiftStdlib 5.0, *) +@_alwaysEmitIntoClient @_transparent +public func withTemporaryArray( + of type: Element.Type, + capacity: Int, + _ body: (inout TemporaryArray) throws(E) -> R +) throws(E) -> R { + precondition(capacity >= 0, "Array capacity must be nonnegative") + let byteCount = capacity * MemoryLayout.stride + if byteCount <= _temporaryArrayStackByteLimit { + // Small enough: carve the initial buffer out of the stack. + return try _withUnsafeTemporaryAllocation( + of: Element.self, capacity: capacity + ) { buffer throws(E) in + var array = unsafe TemporaryArray(borrow: buffer) + // `array` is destroyed (running its deinit) when this closure returns, + // before `_withUnsafeTemporaryAllocation` reclaims `buffer`. + return try body(&array) + } + } + // Too large for the stack budget: allocate the initial buffer on the heap. + var array = TemporaryArray(capacity: capacity) + return try body(&array) +} + +#endif diff --git a/Tests/BasicContainersTests/TemporaryArrayTests.swift b/Tests/BasicContainersTests/TemporaryArrayTests.swift new file mode 100644 index 000000000..38d589ccd --- /dev/null +++ b/Tests/BasicContainersTests/TemporaryArrayTests.swift @@ -0,0 +1,369 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +import XCTest +#if COLLECTIONS_SINGLE_MODULE +import Collections +#else +import _CollectionsTestSupport +import ContainersPreview +import BasicContainers +#endif + +#if compiler(>=6.4) && UnstableContainersPreview + +/// Copies the contents of a span into an array (spans aren't `Sequence`s). +@available(SwiftStdlib 5.0, *) +func materialize(_ span: Span) -> [T] { + var result: [T] = [] + for i in span.indices { result.append(span[i]) } + return result +} + +/// A reference-typed element that counts live instances, so tests can catch +/// leaks (under-release) and double-frees (over-release) in `TemporaryArray`'s +/// stack/heap ownership handling. +@available(SwiftStdlib 5.0, *) +final class Tracker { + nonisolated(unsafe) static var liveCount = 0 + let value: Int + init(_ value: Int) { + self.value = value + Tracker.liveCount += 1 + } + deinit { Tracker.liveCount -= 1 } +} + +@available(SwiftStdlib 5.0, *) +final class TemporaryArrayTests: XCTestCase { + func test_staysWithinReservedCapacity() { + withTemporaryArray(of: Int.self, capacity: 8) { array in + XCTAssertEqual(array.capacity, 8) + for i in 0 ..< 8 { array.append(i) } + XCTAssertEqual(array.count, 8) + XCTAssertEqual(array.capacity, 8, "Should not reallocate within capacity") + XCTAssertEqual(materialize(array.span), Array(0 ..< 8)) + } + } + + func test_growsPastReservedCapacity() { + withTemporaryArray(of: Int.self, capacity: 4) { array in + for i in 0 ..< 4 { array.append(i) } + XCTAssertEqual(array.capacity, 4) + array.append(4) // overflow the initial buffer + XCTAssertGreaterThanOrEqual(array.capacity, 5) + XCTAssertEqual(materialize(array.span), Array(0 ..< 5)) + } + } + + func test_zeroReservedCapacityGrowsOnAppend() { + withTemporaryArray(of: Int.self, capacity: 0) { array in + XCTAssertEqual(array.capacity, 0) + array.append(42) + XCTAssertGreaterThanOrEqual(array.capacity, 1) + XCTAssertEqual(materialize(array.span), [42]) + } + } + + func test_largeInitialCapacity() { + withTemporaryArray(of: Int.self, capacity: 200) { array in + XCTAssertGreaterThanOrEqual(array.capacity, 200) + array.append(copying: 0 ..< 200) + XCTAssertEqual(materialize(array.span), Array(0 ..< 200)) + } + } + + func test_noLeakOrDoubleFree_withinCapacity() { + Tracker.liveCount = 0 + withTemporaryArray(of: Tracker.self, capacity: 8) { array in + for i in 0 ..< 5 { array.append(Tracker(i)) } + XCTAssertEqual(Tracker.liveCount, 5) + } + XCTAssertEqual(Tracker.liveCount, 0, "Elements in the seed buffer must be released exactly once") + } + + func test_noLeakOrDoubleFree_afterGrowth() { + Tracker.liveCount = 0 + withTemporaryArray(of: Tracker.self, capacity: 2) { array in + for i in 0 ..< 10 { array.append(Tracker(i)) } // forces growth/reallocation + XCTAssertEqual(Tracker.liveCount, 10) + } + XCTAssertEqual(Tracker.liveCount, 0, "Elements must survive growth and be released exactly once") + } + + func test_take_movesContentsIntoUniqueArray() { + Tracker.liveCount = 0 + var escaped: UniqueArray = withTemporaryArray( + of: Tracker.self, capacity: 4 + ) { array in + for i in 0 ..< 6 { array.append(Tracker(i)) } // grows past capacity + return array.take() + } + XCTAssertEqual(escaped.count, 6) + XCTAssertEqual(Tracker.liveCount, 6) + XCTAssertEqual(materialize(escaped.span).map { $0.value }, Array(0 ..< 6)) + escaped.removeAll() + XCTAssertEqual(Tracker.liveCount, 0) + } + + func test_take_withinReservedCapacity() { + Tracker.liveCount = 0 + var escaped: UniqueArray = withTemporaryArray( + of: Tracker.self, capacity: 8 + ) { array in + for i in 0 ..< 3 { array.append(Tracker(i)) } // never grows + return array.take() + } + XCTAssertEqual(materialize(escaped.span).map { $0.value }, [0, 1, 2]) + XCTAssertEqual(Tracker.liveCount, 3) + escaped.removeAll() + XCTAssertEqual(Tracker.liveCount, 0) + } + + /// The motivating use case: map/compactMap over an arbitrary sequence using a + /// stack-reserved scratch buffer sized from `underestimatedCount`. + func test_useCase_compactMapOverSequence() { + func compactMapped( + _ source: S, _ transform: (S.Element) -> Int? + ) -> UniqueArray { + withTemporaryArray( + of: Int.self, capacity: source.underestimatedCount + ) { scratch in + for x in source { + if let y = transform(x) { scratch.append(y) } + } + return scratch.take() + } + } + + let result = compactMapped(0 ..< 100) { $0.isMultiple(of: 3) ? $0 * 2 : nil } + XCTAssertEqual( + materialize(result.span).map { $0 }, + (0 ..< 100).compactMap { $0.isMultiple(of: 3) ? $0 * 2 : nil }) + } + + func test_appendCopyingSequence_grows() { + withTemporaryArray(of: Int.self, capacity: 2) { array in + array.append(copying: 0 ..< 50) + XCTAssertGreaterThanOrEqual(array.capacity, 50) + XCTAssertEqual(materialize(array.span), Array(0 ..< 50)) + } + } + + func test_removalsAndInsertions() { + withTemporaryArray(of: Int.self, capacity: 16) { array in + array.append(copying: 0 ..< 10) + array.removeSubrange(2 ..< 5) // remove 2,3,4 + XCTAssertEqual(materialize(array.span), [0, 1, 5, 6, 7, 8, 9]) + array.insert(99, at: 2) + XCTAssertEqual(materialize(array.span), [0, 1, 99, 5, 6, 7, 8, 9]) + XCTAssertEqual(array.removeLast(), 9) + XCTAssertEqual(array.popLast(), 8) + XCTAssertEqual(materialize(array.span), [0, 1, 99, 5, 6, 7]) + } + } + + func test_conformsToContainerProtocols() { + // Exercise the read-side Container conformance via a generic function. + func sum & ~Copyable & ~Escapable>( + _ c: borrowing C + ) -> Int { + var total = 0 + var i = c.startIndex + while true { + let span = c.nextSpan(after: &i) + if span.isEmpty { break } + for j in span.indices { total += span[j] } + } + return total + } + withTemporaryArray(of: Int.self, capacity: 4) { array in + array.append(copying: 1 ... 10) // spills + XCTAssertEqual(sum(array), 55) + } + } + + // MARK: - Newly mirrored SE-0527 API + + func test_heapBackedInitCapacity() { + var array = TemporaryArray(capacity: 4) + XCTAssertEqual(array.capacity, 4) + array.append(copying: 0 ..< 4) + XCTAssertEqual(materialize(array.span), Array(0 ..< 4)) + } + + func test_initCapacityInitializingWith() { + let escaped: UniqueArray = { + var array = TemporaryArray(capacity: 8) { target in + for i in 0 ..< 5 { target.append(i * 10) } + } + XCTAssertEqual(materialize(array.span), [0, 10, 20, 30, 40]) + return array.take() + }() + XCTAssertEqual(materialize(escaped.span), [0, 10, 20, 30, 40]) + } + + func test_appendCopyingBuffer() { + withTemporaryArray(of: Int.self, capacity: 2) { array in + var source = [10, 20, 30, 40] + source.withUnsafeBufferPointer { buffer in + array.append(copying: buffer) // spills + } + XCTAssertEqual(materialize(array.span), [10, 20, 30, 40]) + } + } + + func test_appendMovingOutputSpan() { + withTemporaryArray(of: Int.self, capacity: 8) { array in + array.append(0) + withTemporaryArray(of: Int.self, capacity: 4) { other in + other.append(copying: [1, 2, 3]) + other.edit { span in + array.append(moving: &span) + XCTAssertEqual(span.count, 0) + } + } + XCTAssertEqual(materialize(array.span), [0, 1, 2, 3]) + } + } + + func test_removeAtAndRemoveLastK() { + withTemporaryArray(of: Int.self, capacity: 16) { array in + array.append(copying: 0 ..< 8) + XCTAssertEqual(array.remove(at: 3), 3) + XCTAssertEqual(materialize(array.span), [0, 1, 2, 4, 5, 6, 7]) + array.removeLast(2) + XCTAssertEqual(materialize(array.span), [0, 1, 2, 4, 5]) + } + } + + func test_removeSubrangeRangeExpression() { + withTemporaryArray(of: Int.self, capacity: 16) { array in + array.append(copying: 0 ..< 10) + array.removeSubrange(7...) + XCTAssertEqual(materialize(array.span), Array(0 ..< 7)) + array.removeSubrange(..<2) + XCTAssertEqual(materialize(array.span), Array(2 ..< 7)) + } + } + + func test_replaceSubrangeCopying() { + withTemporaryArray(of: Int.self, capacity: 4) { array in + array.append(copying: 0 ..< 5) // spills + array.replaceSubrange(1 ..< 3, copying: [90, 91, 92, 93]) + XCTAssertEqual(materialize(array.span), [0, 90, 91, 92, 93, 3, 4]) + } + } + + func test_insertCopyingCollection() { + withTemporaryArray(of: Int.self, capacity: 8) { array in + array.append(copying: [0, 1, 2]) + array.insert(copying: [97, 98, 99], at: 1) + XCTAssertEqual(materialize(array.span), [0, 97, 98, 99, 1, 2]) + } + } + + func test_isTriviallyIdentical() { + withTemporaryArray(of: Int.self, capacity: 4) { a in + a.append(copying: [1, 2, 3]) + XCTAssertTrue(a.isTriviallyIdentical(to: a)) + withTemporaryArray(of: Int.self, capacity: 4) { b in + b.append(copying: [1, 2, 3]) + XCTAssertFalse(a.isTriviallyIdentical(to: b)) + } + } + } + + func test_appendRepeating() { + withTemporaryArray(of: Int.self, capacity: 8) { array in + array.append(3) + array.append(repeating: 7, count: 4) + array.append(repeating: 0, count: 0) // no-op + XCTAssertEqual(materialize(array.span), [3, 7, 7, 7, 7]) + } + } + + func test_appendRepeating_growsAndSpills() { + withTemporaryArray(of: Int.self, capacity: 2) { array in + array.append(repeating: 9, count: 50) + XCTAssertGreaterThanOrEqual(array.capacity, 50) + XCTAssertEqual(materialize(array.span), Array(repeating: 9, count: 50)) + } + } + + func test_equatable() { + withTemporaryArray(of: Int.self, capacity: 4) { a in + a.append(copying: [1, 2, 3]) + withTemporaryArray(of: Int.self, capacity: 8) { b in + b.append(copying: [1, 2, 3]) // equal contents, distinct storage + XCTAssertTrue(a == b) + b.append(4) + XCTAssertTrue(a != b) + } + } + } + + func test_hashable() { + func hash(_ body: (inout TemporaryArray) -> Void) -> Int { + withTemporaryArray(of: Int.self, capacity: 4) { array in + body(&array) + var hasher = Hasher() + array.hash(into: &hasher) + return hasher.finalize() + } + } + XCTAssertEqual( + hash { $0.append(copying: [1, 2, 3]) }, + hash { $0.append(copying: [1, 2, 3]) }) + } + + func test_description() { + withTemporaryArray(of: Int.self, capacity: 4) { array in + array.append(copying: [1, 2, 3]) + XCTAssertEqual(array.description, "<3 items>") + XCTAssertEqual(array.debugDescription, "<3 items>") + } + } + + func test_clone_thenTake() { + Tracker.liveCount = 0 + var escaped: UniqueArray = withTemporaryArray( + of: Tracker.self, capacity: 8 + ) { array in + for i in 0 ..< 3 { array.append(Tracker(i)) } + // clone is only valid for Copyable elements at compile time; Tracker is a + // class (Copyable reference), so this exercises the reference-copy path. + var copy = array.clone() + XCTAssertEqual(copy.count, 3) + XCTAssertFalse(array.isTriviallyIdentical(to: copy)) + return copy.take() + } + XCTAssertEqual(materialize(escaped.span).map { $0.value }, [0, 1, 2]) + XCTAssertEqual(Tracker.liveCount, 3) + escaped.removeAll() + XCTAssertEqual(Tracker.liveCount, 0) + } + + func test_cloneCapacity() { + withTemporaryArray(of: Int.self, capacity: 4) { array in + array.append(copying: [1, 2, 3]) + var big = array.clone(capacity: 16) + XCTAssertEqual(big.capacity, 16) + XCTAssertEqual(materialize(big.span), [1, 2, 3]) + big.append(4) + XCTAssertEqual(materialize(big.span), [1, 2, 3, 4]) + } + } +} + +#endif