Skip to content

Commit c6dd3e8

Browse files
committed
Add TemporaryArray: a stack-seeded, heap-spilling scratch array
TemporaryArray is a ~Copyable and ~Escapable dynamic array whose initial storage is a borrowed buffer (most usefully a stack allocation vended by withTemporaryArray(of:capacity:_:)) and which transparently spills over into freshly allocated heap storage once it outgrows that seed buffer. As long as the element count stays within a seed buffer that fit on the stack, the array incurs no heap traffic at all. Because it can hold a dependency on borrowed (stack) memory, the type is non-escapable: instances cannot outlive the scope that provides their initial buffer. Contents are lifted out into an owning container with take(), which transfers the heap buffer in O(1) if the array has already spilled, or moves the elements into a fresh UniqueArray otherwise. The API mirrors UniqueArray (SE-0527), minus the pieces that don't apply to a non-escapable scratch type (DynamicContainer, MutableContainer, and RangeReplaceableContainer conformances plus the Drain-based consumer), plus members unique to its borrow-then-spill design: take(), clone(), and append(repeating:count:). The seed-buffer initializer is kept internal for now, with withTemporaryArray as the sole entry point. Equatable/Hashable and CustomStringConvertible/CustomDebugStringConvertible are implemented as methods with FIXMEs to add the conformances once those protocols support ~Copyable/~Escapable types. The Collection/RangeReplaceable surface and container-protocol conformances are gated behind the UnstableContainersPreview trait, mirroring UniqueArray; the core API builds in the default configuration. DocC coverage is added for the type and the withTemporaryArray entry point.
1 parent 90f2654 commit c6dd3e8

11 files changed

Lines changed: 1992 additions & 0 deletions

Sources/BasicContainers/BasicContainers.docc/BasicContainers.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,35 @@ Unlike ``InlineArray``, the capacity of a ``RigidArray`` is not part of its type
6060

6161
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.
6262

63+
### struct TemporaryArray
64+
65+
``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.
66+
67+
```swift
68+
let sum = withTemporaryArray(of: Int.self, capacity: 64) { scratch in
69+
for x in numbers where isHot(x) {
70+
scratch.append(x * x)
71+
}
72+
var total = 0
73+
for i in scratch.indices { total += scratch[i] }
74+
return total
75+
}
76+
```
77+
78+
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.
79+
80+
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.
81+
82+
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).
83+
6384
## Topics
6485

6586
### Types
6687

6788
- ``UniqueArray``
6889
- ``RigidArray``
90+
- ``TemporaryArray``
91+
92+
### Functions
93+
94+
- ``withTemporaryArray(of:capacity:_:)``
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# ``BasicContainers/TemporaryArray``
2+
3+
## Topics
4+
5+
### Creating a Temporary Array
6+
7+
- ``withTemporaryArray(of:capacity:_:)``
8+
- ``init()``
9+
- ``init(capacity:)``
10+
- ``init(capacity:initializingWith:)``
11+
12+
### Inspecting a Temporary Array
13+
14+
- ``isEmpty``
15+
- ``count``
16+
- ``capacity``
17+
- ``freeCapacity``
18+
- ``isTriviallyIdentical(to:)``
19+
20+
### Indices
21+
22+
- ``Index``
23+
- ``startIndex``
24+
- ``endIndex``
25+
- ``indices``
26+
27+
### Accessing Elements
28+
29+
- ``subscript(_:)``
30+
- ``swapAt(_:_:)``
31+
- ``edit(_:)``
32+
33+
### Memory Management
34+
35+
- ``reallocate(capacity:)``
36+
- ``reserveCapacity(_:)``
37+
38+
### Moving and Copying Out
39+
40+
- ``take()``
41+
- ``clone()``
42+
- ``clone(capacity:)``
43+
44+
### Spans
45+
46+
- ``span``
47+
- ``mutableSpan``
48+
- ``nextSpan(after:maximumCount:)``
49+
- ``nextMutableSpan(after:maximumCount:)``
50+
- ``previousSpan(before:maximumCount:)``
51+
52+
### Appending Items
53+
54+
- ``append(_:)``
55+
- ``append(addingCount:initializingWith:)``
56+
- ``append(repeating:count:)``
57+
- ``append(moving:)-(UnsafeMutableBufferPointer<Element>)``
58+
- ``append(moving:)-(OutputSpan<Element>)``
59+
- ``append(copying:)-(Sequence<Element>)``
60+
- ``append(copying:)-(Span<Element>)``
61+
- ``append(copying:)-(UnsafeBufferPointer<Element>)``
62+
- ``append(copying:)-(UnsafeMutableBufferPointer<Element>)``

Sources/BasicContainers/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ target_sources(${module_name} PRIVATE
5252
"UniqueArray/UniqueArray+Insertions.swift"
5353
"UniqueArray/UniqueArray+Removals.swift"
5454
"UniqueArray/UniqueArray+Replacements.swift"
55+
"TemporaryArray/TemporaryArray.swift"
56+
"TemporaryArray/TemporaryArray+Append.swift"
57+
"TemporaryArray/TemporaryArray+Container.swift"
58+
"TemporaryArray/TemporaryArray+Descriptions.swift"
59+
"TemporaryArray/TemporaryArray+Equatable.swift"
60+
"TemporaryArray/TemporaryArray+Hashable.swift"
61+
"TemporaryArray/TemporaryArray+RangeReplaceable.swift"
5562
"HashTable/_HTable.swift"
5663
"HashTable/_HTable+Bitmap.swift"
5764
"HashTable/_HTable+Bucket.swift"
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Collections open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
11+
//
12+
//===----------------------------------------------------------------------===//
13+
14+
#if !COLLECTIONS_SINGLE_MODULE
15+
import InternalCollectionsUtilities
16+
import ContainersPreview
17+
#endif
18+
19+
#if compiler(>=6.2)
20+
21+
@available(SwiftStdlib 5.0, *)
22+
extension TemporaryArray where Element: ~Copyable {
23+
/// Adds an element to the end of the array, growing (and, if needed, spilling
24+
/// to the heap) to make room.
25+
///
26+
/// - Complexity: O(1), amortized over many invocations on the same array.
27+
@_alwaysEmitIntoClient
28+
public mutating func append(_ item: consuming Element) {
29+
_ensureFreeCapacity(1)
30+
unsafe _storage.initializeElement(at: _count, to: item)
31+
_count &+= 1
32+
}
33+
34+
/// Appends a given number of items to the end of the array by populating an
35+
/// output span, growing (and, if needed, spilling to the heap) to make room.
36+
///
37+
/// The closure may initialize fewer than `newItemCount` items; the array
38+
/// gains exactly as many as the closure adds before it returns or throws.
39+
///
40+
/// - Complexity: O(`newItemCount`), amortized.
41+
@_alwaysEmitIntoClient
42+
public mutating func append<E: Error>(
43+
addingCount newItemCount: Int,
44+
initializingWith initializer: (inout OutputSpan<Element>) throws(E) -> Void
45+
) throws(E) {
46+
precondition(newItemCount >= 0, "Cannot add a negative number of items")
47+
_ensureFreeCapacity(newItemCount)
48+
let buffer = unsafe _freeSpace.extracting(
49+
Range(uncheckedBounds: (0, newItemCount)))
50+
var span = unsafe OutputSpan(buffer: buffer, initializedCount: 0)
51+
defer {
52+
_count &+= span.finalize(for: buffer)
53+
span = OutputSpan()
54+
}
55+
return try initializer(&span)
56+
}
57+
}
58+
59+
@available(SwiftStdlib 5.0, *)
60+
extension TemporaryArray where Element: ~Copyable {
61+
/// Moves the elements of a buffer to the end of this array, leaving the
62+
/// buffer uninitialized.
63+
///
64+
/// - Complexity: O(`items.count`), amortized.
65+
@_alwaysEmitIntoClient
66+
public mutating func append(
67+
moving items: UnsafeMutableBufferPointer<Element>
68+
) {
69+
guard items.count > 0 else { return }
70+
_ensureFreeCapacity(items.count)
71+
let target = unsafe _freeSpace.extracting(
72+
Range(uncheckedBounds: (0, items.count)))
73+
let i = unsafe target.moveInitialize(fromContentsOf: items)
74+
assert(i == items.count)
75+
_count &+= items.count
76+
}
77+
78+
/// Moves the elements of an output span to the end of this array, leaving the
79+
/// span empty.
80+
///
81+
/// - Complexity: O(`items.count`), amortized.
82+
@_alwaysEmitIntoClient
83+
public mutating func append(
84+
moving items: inout OutputSpan<Element>
85+
) {
86+
items.withUnsafeMutableBufferPointer { buffer, count in
87+
let source = unsafe buffer.extracting(Range(uncheckedBounds: (0, count)))
88+
unsafe self.append(moving: source)
89+
count = 0
90+
}
91+
}
92+
}
93+
94+
@available(SwiftStdlib 5.0, *)
95+
extension TemporaryArray /*where Element: Copyable*/ {
96+
/// Appends `count` copies of `repeatedValue` to the end of the array, growing
97+
/// (and, if needed, spilling to the heap) to make room.
98+
///
99+
/// This is `TemporaryArray`'s analogue of `UniqueArray`'s
100+
/// `init(repeating:count:)`: it's an append rather than an initializer,
101+
/// because a `TemporaryArray` is meant to be seeded (typically on the stack)
102+
/// via `withTemporaryArray` and then filled.
103+
///
104+
/// - Complexity: O(`count`), amortized.
105+
@_alwaysEmitIntoClient
106+
public mutating func append(repeating repeatedValue: Element, count: Int) {
107+
precondition(count >= 0, "Cannot add a negative number of items")
108+
guard count > 0 else { return }
109+
_ensureFreeCapacity(count)
110+
let target = unsafe _freeSpace.extracting(
111+
Range(uncheckedBounds: (0, count)))
112+
unsafe target.initialize(repeating: repeatedValue)
113+
_count &+= count
114+
}
115+
116+
/// Copies the elements of a buffer to the end of this array.
117+
///
118+
/// - Complexity: O(`newElements.count`), amortized.
119+
@_alwaysEmitIntoClient
120+
public mutating func append(
121+
copying newElements: UnsafeBufferPointer<Element>
122+
) {
123+
guard newElements.count > 0 else { return }
124+
_ensureFreeCapacity(newElements.count)
125+
let target = unsafe _freeSpace.extracting(
126+
Range(uncheckedBounds: (0, newElements.count)))
127+
_ = unsafe target.initialize(fromContentsOf: newElements)
128+
_count &+= newElements.count
129+
}
130+
131+
/// Copies the elements of a buffer to the end of this array.
132+
///
133+
/// - Complexity: O(`newElements.count`), amortized.
134+
@_alwaysEmitIntoClient
135+
public mutating func append(
136+
copying items: UnsafeMutableBufferPointer<Element>
137+
) {
138+
unsafe self.append(copying: UnsafeBufferPointer(items))
139+
}
140+
141+
/// Copies the elements of a span to the end of this array.
142+
///
143+
/// - Complexity: O(`newElements.count`), amortized.
144+
@_alwaysEmitIntoClient
145+
public mutating func append(copying newElements: Span<Element>) {
146+
guard newElements.count > 0 else { return }
147+
_ensureFreeCapacity(newElements.count)
148+
let target = unsafe _freeSpace.extracting(
149+
Range(uncheckedBounds: (0, newElements.count)))
150+
unsafe newElements.withUnsafeBufferPointer { source in
151+
_ = unsafe target.initialize(fromContentsOf: source)
152+
}
153+
_count &+= newElements.count
154+
}
155+
156+
/// Copies the elements of a sequence to the end of this array.
157+
///
158+
/// If the sequence provides only a loose `underestimatedCount`, the array's
159+
/// storage may need to be resized more than once (potentially spilling to the
160+
/// heap along the way).
161+
///
162+
/// - Complexity: O(*m*), where *m* is the length of `newElements`, amortized.
163+
@_alwaysEmitIntoClient
164+
public mutating func append(copying newElements: some Sequence<Element>) {
165+
let done: Void? = newElements.withContiguousStorageIfAvailable { buffer in
166+
_ensureFreeCapacity(buffer.count)
167+
let target = unsafe _freeSpace.extracting(
168+
Range(uncheckedBounds: (0, buffer.count)))
169+
_ = unsafe target.initialize(fromContentsOf: buffer)
170+
_count &+= buffer.count
171+
return
172+
}
173+
if done != nil { return }
174+
175+
_ensureFreeCapacity(newElements.underestimatedCount)
176+
for item in newElements {
177+
append(item)
178+
}
179+
}
180+
}
181+
182+
#if compiler(>=6.4) && UnstableContainersPreview
183+
@available(SwiftStdlib 5.0, *)
184+
extension TemporaryArray where Element: ~Copyable {
185+
/// Appends all the items generated by a producer to the end of this array,
186+
/// growing (and, if needed, spilling to the heap) as it goes.
187+
///
188+
/// This is the building block for collecting the result of mapping, filtering
189+
/// or otherwise transforming an arbitrary generative sequence whose final
190+
/// length isn't known in advance. The producer's `underestimatedCount` is
191+
/// used to size each bulk append.
192+
///
193+
/// - Complexity: O(*n*) where *n* is the number of generated items, amortized.
194+
@_alwaysEmitIntoClient
195+
public mutating func append<
196+
E: Error,
197+
P: Producer<Element, E> & ~Copyable & ~Escapable
198+
>(
199+
from producer: inout P
200+
) throws(E)
201+
where P.Element: ~Copyable {
202+
var done = false
203+
while !done {
204+
let c = Swift.max(producer.underestimatedCount, 1)
205+
_ensureFreeCapacity(c)
206+
try self.append(addingCount: freeCapacity) { target throws(E) in
207+
while !target.isFull, !done {
208+
done = try !producer.generate(into: &target)
209+
}
210+
}
211+
}
212+
}
213+
}
214+
#endif
215+
216+
#endif

0 commit comments

Comments
 (0)