authorgravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2025-08-23 15:35:17+02:00
committergravatar for mail@isaacfreund.comIsaac Freund <mail@isaacfreund.com> 2025-08-25 20:07:59+02:00
log3e77317261e3da50ac55be0c14bc00192ee93166
treec653fb7267fbf4c31a707fe6589ebd9c8902df32
parent47a2f2ddae9cc47ff6df7a71060bbb3f5f20f2e8
signaturelock-open Commit is signed but in an unrecognized format.

std: add a Deque data structure

This is my penance for baiting andrew into deleting the existing generic queue data structures with my talk of "too many ring buffers". The new Reader and Writer interfaces are excellent ring buffers for many use cases, but a generic queue container type is now missing. This new double-ended queue, known more succinctly as a deque, is implemented from scratch based on the API design lessons learned from ArrayList over the years. The API is not yet as featureful as ArrayList, but the core functionality is in place and I will be using this in my personal projects shortly. I think it makes sense to add further functions as needed based on real-world use-cases.

2 files changed, 434 insertions(+), 0 deletions(-)

lib/std/deque.zig created+433
...@@ -0,0 +1,433 @@
1const std = @import("std");
2const assert = std.debug.assert;
3const Allocator = std.mem.Allocator;
4
5/// A contiguous, growable, double-ended queue.
6///
7/// Pushing/popping items from either end of the queue is O(1).
8pub fn Deque(comptime T: type) type {
9 return struct {
10 const Self = @This();
11
12 /// A ring buffer.
13 buffer: []T,
14 /// The index in buffer where the first item in the logical deque is stored.
15 head: usize,
16 /// The number of items stored in the logical deque.
17 len: usize,
18
19 /// A Deque containing no elements.
20 pub const empty: Self = .{
21 .buffer = &.{},
22 .head = 0,
23 .len = 0,
24 };
25
26 /// Initialize with capacity to hold `capacity` elements.
27 /// The resulting capacity will equal `capacity` exactly.
28 /// Deinitialize with `deinit`.
29 pub fn initCapacity(gpa: Allocator, capacity: usize) Allocator.Error!Self {
30 var deque: Self = .empty;
31 try deque.ensureTotalCapacityPrecise(gpa, capacity);
32 return deque;
33 }
34
35 /// Initialize with externally-managed memory. The buffer determines the
36 /// capacity and the deque is initially empty.
37 ///
38 /// When initialized this way, all functions that accept an Allocator
39 /// argument cause illegal behavior.
40 pub fn initBuffer(buffer: []T) Self {
41 return .{
42 .buffer = buffer,
43 .head = 0,
44 .len = 0,
45 };
46 }
47
48 /// Release all allocated memory.
49 pub fn deinit(deque: *Self, gpa: Allocator) void {
50 gpa.free(deque.buffer);
51 deque.* = undefined;
52 }
53
54 /// Modify the deque so that it can hold at least `new_capacity` items.
55 /// Implements super-linear growth to achieve amortized O(1) push/pop operations.
56 /// Invalidates element pointers if additional memory is needed.
57 pub fn ensureTotalCapacity(deque: *Self, gpa: Allocator, new_capacity: usize) Allocator.Error!void {
58 if (deque.buffer.len >= new_capacity) return;
59 return deque.ensureTotalCapacityPrecise(gpa, growCapacity(deque.buffer.len, new_capacity));
60 }
61
62 /// If the current capacity is less than `new_capacity`, this function will
63 /// modify the deque so that it can hold exactly `new_capacity` items.
64 /// Invalidates element pointers if additional memory is needed.
65 pub fn ensureTotalCapacityPrecise(deque: *Self, gpa: Allocator, new_capacity: usize) Allocator.Error!void {
66 if (deque.buffer.len >= new_capacity) return;
67 const old_buffer = deque.buffer;
68 if (gpa.remap(old_buffer, new_capacity)) |new_buffer| {
69 // If the items wrap around the end of the buffer we need to do
70 // a memcpy to prevent a gap after resizing the buffer.
71 if (deque.head > old_buffer.len - deque.len) {
72 // The gap splits the items in the deque into head and tail parts.
73 // Choose the shorter part to copy.
74 const head = new_buffer[deque.head..old_buffer.len];
75 const tail = new_buffer[0 .. deque.len - head.len];
76 if (head.len > tail.len and new_buffer.len - old_buffer.len > tail.len) {
77 @memcpy(new_buffer[old_buffer.len..][0..tail.len], tail);
78 } else {
79 // In this case overlap is possible if e.g. the capacity increase is 1
80 // and head.len is greater than 1.
81 deque.head = new_buffer.len - head.len;
82 @memmove(new_buffer[deque.head..][0..head.len], head);
83 }
84 }
85 deque.buffer = new_buffer;
86 } else {
87 const new_buffer = try gpa.alloc(T, new_capacity);
88 if (deque.head < old_buffer.len - deque.len) {
89 @memcpy(new_buffer[0..deque.len], old_buffer[deque.head..][0..deque.len]);
90 } else {
91 const head = old_buffer[deque.head..];
92 const tail = old_buffer[0 .. deque.len - head.len];
93 @memcpy(new_buffer[0..head.len], head);
94 @memcpy(new_buffer[head.len..][0..tail.len], tail);
95 }
96 deque.head = 0;
97 deque.buffer = new_buffer;
98 gpa.free(old_buffer);
99 }
100 }
101
102 /// Modify the deque so that it can hold at least `additional_count` **more** items.
103 /// Invalidates element pointers if additional memory is needed.
104 pub fn ensureUnusedCapacity(
105 deque: *Self,
106 gpa: Allocator,
107 additional_count: usize,
108 ) Allocator.Error!void {
109 return deque.ensureTotalCapacity(gpa, try addOrOom(deque.len, additional_count));
110 }
111
112 /// Add one item to the front of the deque.
113 ///
114 /// Invalidates element pointers if additional memory is needed.
115 pub fn pushFront(deque: *Self, gpa: Allocator, item: T) error{OutOfMemory}!void {
116 try deque.ensureUnusedCapacity(gpa, 1);
117 deque.pushFrontAssumeCapacity(item);
118 }
119
120 /// Add one item to the front of the deque.
121 ///
122 /// Never invalidates element pointers.
123 ///
124 /// If the deque lacks unused capacity for the additional item, returns
125 /// `error.OutOfMemory`.
126 pub fn pushFrontBounded(deque: *Self, item: T) error{OutOfMemory}!void {
127 if (deque.buffer.len - deque.len == 0) return error.OutOfMemory;
128 return deque.pushFrontAssumeCapacity(item);
129 }
130
131 /// Add one item to the front of the deque.
132 ///
133 /// Never invalidates element pointers.
134 ///
135 /// Asserts that the deque can hold one additional item.
136 pub fn pushFrontAssumeCapacity(deque: *Self, item: T) void {
137 assert(deque.len < deque.buffer.len);
138 if (deque.head == 0) {
139 deque.head = deque.buffer.len;
140 }
141 deque.head -= 1;
142 deque.buffer[deque.head] = item;
143 deque.len += 1;
144 }
145
146 /// Add one item to the front of the deque.
147 ///
148 /// Invalidates element pointers if additional memory is needed.
149 pub fn pushBack(deque: *Self, gpa: Allocator, item: T) error{OutOfMemory}!void {
150 try deque.ensureUnusedCapacity(gpa, 1);
151 deque.pushBackAssumeCapacity(item);
152 }
153
154 /// Add one item to the back of the deque.
155 ///
156 /// Never invalidates element pointers.
157 ///
158 /// If the deque lacks unused capacity for the additional item, returns
159 /// `error.OutOfMemory`.
160 pub fn pushBackBounded(deque: *Self, item: T) error{OutOfMemory}!void {
161 if (deque.buffer.len - deque.len == 0) return error.OutOfMemory;
162 deque.pushBackAssumeCapacity(item);
163 }
164
165 /// Add one item to the back of the deque.
166 ///
167 /// Never invalidates element pointers.
168 ///
169 /// Asserts that the deque can hold one additional item.
170 pub fn pushBackAssumeCapacity(deque: *Self, item: T) void {
171 assert(deque.len < deque.buffer.len);
172 const buffer_index = deque.bufferIndex(deque.len);
173 deque.buffer[buffer_index] = item;
174 deque.len += 1;
175 }
176
177 /// Return the first item in the deque or null if empty.
178 pub fn front(deque: *const Self) ?T {
179 if (deque.len == 0) return null;
180 return deque.buffer[deque.head];
181 }
182
183 /// Return the last item in the deque or null if empty.
184 pub fn back(deque: *const Self) ?T {
185 if (deque.len == 0) return null;
186 return deque.buffer[deque.bufferIndex(deque.len - 1)];
187 }
188
189 /// Return the item at the given index in the deque.
190 ///
191 /// The first item in the queue is at index 0.
192 ///
193 /// Asserts that the index is in-bounds.
194 pub fn at(deque: *const Self, index: usize) T {
195 assert(index < deque.len);
196 return deque.buffer[deque.bufferIndex(index)];
197 }
198
199 /// Remove and return the first item in the deque or null if empty.
200 pub fn popFront(deque: *Self) ?T {
201 if (deque.len == 0) return null;
202 const pop_index = deque.head;
203 deque.head = deque.bufferIndex(1);
204 deque.len -= 1;
205 return deque.buffer[pop_index];
206 }
207
208 /// Remove and return the last item in the deque or null if empty.
209 pub fn popBack(deque: *Self) ?T {
210 if (deque.len == 0) return null;
211 deque.len -= 1;
212 return deque.buffer[deque.bufferIndex(deque.len)];
213 }
214
215 pub const Iterator = struct {
216 deque: *const Self,
217 index: usize,
218
219 pub fn next(it: *Iterator) ?T {
220 if (it.index < it.deque.len) {
221 defer it.index += 1;
222 return it.deque.at(it.index);
223 } else {
224 return null;
225 }
226 }
227 };
228
229 /// Iterates over all items in the deque in order from front to back.
230 pub fn iterator(deque: *const Self) Iterator {
231 return .{ .deque = deque, .index = 0 };
232 }
233
234 /// Returns the index in `buffer` where the element at the given
235 /// index in the logical deque is stored.
236 fn bufferIndex(deque: *const Self, index: usize) usize {
237 // This function is written in this way to avoid overflow and
238 // expensive division.
239 const head_len = deque.buffer.len - deque.head;
240 if (index < head_len) {
241 return deque.head + index;
242 } else {
243 return index - head_len;
244 }
245 }
246
247 const init_capacity: comptime_int = @max(1, std.atomic.cache_line / @sizeOf(T));
248
249 /// Called when memory growth is necessary. Returns a capacity larger than
250 /// minimum that grows super-linearly.
251 fn growCapacity(current: usize, minimum: usize) usize {
252 var new = current;
253 while (true) {
254 new +|= new / 2 + init_capacity;
255 if (new >= minimum) return new;
256 }
257 }
258 };
259}
260
261/// Integer addition returning `error.OutOfMemory` on overflow.
262fn addOrOom(a: usize, b: usize) error{OutOfMemory}!usize {
263 const result, const overflow = @addWithOverflow(a, b);
264 if (overflow != 0) return error.OutOfMemory;
265 return result;
266}
267
268test "basic" {
269 const testing = std.testing;
270 const gpa = testing.allocator;
271
272 var q: Deque(u32) = .empty;
273 defer q.deinit(gpa);
274
275 try testing.expectEqual(null, q.popFront());
276 try testing.expectEqual(null, q.popBack());
277
278 try q.pushBack(gpa, 1);
279 try q.pushBack(gpa, 2);
280 try q.pushBack(gpa, 3);
281 try q.pushFront(gpa, 0);
282
283 try testing.expectEqual(0, q.popFront());
284 try testing.expectEqual(1, q.popFront());
285 try testing.expectEqual(3, q.popBack());
286 try testing.expectEqual(2, q.popFront());
287 try testing.expectEqual(null, q.popFront());
288 try testing.expectEqual(null, q.popBack());
289}
290
291test "buffer" {
292 const testing = std.testing;
293
294 var buffer: [4]u32 = undefined;
295 var q: Deque(u32) = .initBuffer(&buffer);
296
297 try testing.expectEqual(null, q.popFront());
298 try testing.expectEqual(null, q.popBack());
299
300 try q.pushBackBounded(1);
301 try q.pushBackBounded(2);
302 try q.pushBackBounded(3);
303 try q.pushFrontBounded(0);
304 try testing.expectError(error.OutOfMemory, q.pushBackBounded(4));
305
306 try testing.expectEqual(0, q.popFront());
307 try testing.expectEqual(1, q.popFront());
308 try testing.expectEqual(3, q.popBack());
309 try testing.expectEqual(2, q.popFront());
310 try testing.expectEqual(null, q.popFront());
311 try testing.expectEqual(null, q.popBack());
312}
313
314test "slow growth" {
315 const testing = std.testing;
316 const gpa = testing.allocator;
317
318 var q: Deque(i32) = .empty;
319 defer q.deinit(gpa);
320
321 try q.ensureTotalCapacityPrecise(gpa, 1);
322 q.pushBackAssumeCapacity(1);
323 try q.ensureTotalCapacityPrecise(gpa, 2);
324 q.pushFrontAssumeCapacity(0);
325 try q.ensureTotalCapacityPrecise(gpa, 3);
326 q.pushBackAssumeCapacity(2);
327 try q.ensureTotalCapacityPrecise(gpa, 5);
328 q.pushBackAssumeCapacity(3);
329 q.pushFrontAssumeCapacity(-1);
330 try q.ensureTotalCapacityPrecise(gpa, 6);
331 q.pushFrontAssumeCapacity(-2);
332
333 try testing.expectEqual(-2, q.popFront());
334 try testing.expectEqual(-1, q.popFront());
335 try testing.expectEqual(3, q.popBack());
336 try testing.expectEqual(0, q.popFront());
337 try testing.expectEqual(2, q.popBack());
338 try testing.expectEqual(1, q.popBack());
339 try testing.expectEqual(null, q.popFront());
340 try testing.expectEqual(null, q.popBack());
341}
342
343test "fuzz against ArrayList oracle" {
344 try std.testing.fuzz({}, fuzzAgainstArrayList, .{});
345}
346
347test "dumb fuzz against ArrayList oracle" {
348 const testing = std.testing;
349 const gpa = testing.allocator;
350
351 const input = try gpa.alloc(u8, 1024);
352 defer gpa.free(input);
353
354 var prng = std.Random.DefaultPrng.init(testing.random_seed);
355 prng.random().bytes(input);
356
357 try fuzzAgainstArrayList({}, input);
358}
359
360fn fuzzAgainstArrayList(_: void, input: []const u8) anyerror!void {
361 const testing = std.testing;
362 const gpa = testing.allocator;
363
364 var q: Deque(u32) = .empty;
365 defer q.deinit(gpa);
366 var l: std.ArrayList(u32) = .empty;
367 defer l.deinit(gpa);
368
369 if (input.len < 2) return;
370
371 var prng = std.Random.DefaultPrng.init(input[0]);
372 const random = prng.random();
373
374 const Action = enum {
375 push_back,
376 push_front,
377 pop_back,
378 pop_front,
379 grow,
380 /// Sentinel to avoid hardcoding the cast below
381 max,
382 };
383 for (input[1..]) |byte| {
384 switch (@as(Action, @enumFromInt(byte % (@intFromEnum(Action.max))))) {
385 .push_back => {
386 const item = random.int(u8);
387 try testing.expectEqual(
388 l.appendBounded(item),
389 q.pushBackBounded(item),
390 );
391 },
392 .push_front => {
393 const item = random.int(u8);
394 try testing.expectEqual(
395 l.insertBounded(0, item),
396 q.pushFrontBounded(item),
397 );
398 },
399 .pop_back => {
400 try testing.expectEqual(l.pop(), q.popBack());
401 },
402 .pop_front => {
403 try testing.expectEqual(
404 if (l.items.len > 0) l.orderedRemove(0) else null,
405 q.popFront(),
406 );
407 },
408 // Growing by small, random, linear amounts seems to better test
409 // ensureTotalCapacityPrecise(), which is the most complex part
410 // of the Deque implementation.
411 .grow => {
412 const growth = random.int(u3);
413 try l.ensureTotalCapacityPrecise(gpa, l.items.len + growth);
414 try q.ensureTotalCapacityPrecise(gpa, q.len + growth);
415 },
416 .max => unreachable,
417 }
418 try testing.expectEqual(l.getLastOrNull(), q.back());
419 try testing.expectEqual(
420 if (l.items.len > 0) l.items[0] else null,
421 q.front(),
422 );
423 try testing.expectEqual(l.items.len, q.len);
424 try testing.expectEqual(l.capacity, q.buffer.len);
425 {
426 var it = q.iterator();
427 for (l.items) |item| {
428 try testing.expectEqual(item, it.next());
429 }
430 try testing.expectEqual(null, it.next());
431 }
432 }
433}
lib/std/std.zig+1
...@@ -10,6 +10,7 @@ pub const BufMap = @import("buf_map.zig").BufMap;...@@ -10,6 +10,7 @@ pub const BufMap = @import("buf_map.zig").BufMap;
10pub const BufSet = @import("buf_set.zig").BufSet;10pub const BufSet = @import("buf_set.zig").BufSet;
11pub const StaticStringMap = static_string_map.StaticStringMap;11pub const StaticStringMap = static_string_map.StaticStringMap;
12pub const StaticStringMapWithEql = static_string_map.StaticStringMapWithEql;12pub const StaticStringMapWithEql = static_string_map.StaticStringMapWithEql;
13pub const Deque = @import("deque.zig").Deque;
13pub const DoublyLinkedList = @import("DoublyLinkedList.zig");14pub const DoublyLinkedList = @import("DoublyLinkedList.zig");
14pub const DynLib = @import("dynamic_library.zig").DynLib;15pub const DynLib = @import("dynamic_library.zig").DynLib;
15pub const DynamicBitSet = bit_set.DynamicBitSet;16pub const DynamicBitSet = bit_set.DynamicBitSet;