authorgravatar for me@robbielyman.comRobbie Lyman <me@robbielyman.com> 2026-09-09 14:38:09-04:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-10-09 23:14:28+02:00
log1df1f936b3f4b6f2e8e97248d5779cac1f60a911
tree25ba8d52a9cbe9beab28ed36b96c2dd73c9e3419
parentd69634c10d2e4ca4af834a0644136ac1af72cd26

feat(MultiArrayList): Pointer Stability

This commit builds on #19523 to add pointer stability to MultiArrayList, following the lead of the design for HashMap and ArrayList. Since initializing MultiArrayList with a struct literal is much more common within the compiler than for ArrayList, I have chosen to make the field a default init, although I think this is not the best design. Similar to ArrayList, I have chosen to interpret "pointer stability" broadly, meaning that existing pointers to (fields of) elements in the MultiArrayList continue to be valid pointers to the same object. Thus `appendAssumeCapacity()` does not invalidate pointers, but `pop()` and `swap()` do. Closes #19327.

2 files changed, 96 insertions(+), 5 deletions(-)

lib/std/multi_array_list.zig+95-5
......@@ -26,11 +26,13 @@ pub fn MultiArrayList(comptime T: type) type {
2626 bytes: [*]u8 = undefined,
2727 len: usize = 0,
2828 capacity: usize = 0,
29 pointer_stability: std.debug.SafetyLock = .{},
2930
3031 pub const empty: Self = .{
3132 .bytes = undefined,
3233 .len = 0,
3334 .capacity = 0,
35 .pointer_stability = .{},
3436 };
3537
3638 /// Initialize with capacity to hold exactly `num` elements.
......@@ -80,13 +82,16 @@ pub fn MultiArrayList(comptime T: type) type {
8082 ptrs: [field_names.len][*]u8,
8183 len: usize,
8284 capacity: usize,
85 pointer_stability: std.debug.SafetyLock,
8386
8487 pub const empty: Slice = .{
8588 .ptrs = undefined,
8689 .len = 0,
8790 .capacity = 0,
91 .pointer_stability = .{},
8892 };
8993
94 /// The returned pointer may be invalidated by various operations to this Slice.
9095 pub fn items(self: Slice, comptime field: Field) []FieldType(field) {
9196 const F = FieldType(field);
9297 if (self.capacity == 0) {
......@@ -100,7 +105,9 @@ pub fn MultiArrayList(comptime T: type) type {
100105 return casted_ptr[0..self.len];
101106 }
102107
108 /// Invalidates pre-existing pointers to fields of the element at index `index`.
103109 pub fn set(self: *Slice, index: usize, elem: T) void {
110 self.pointer_stability.assertUnlocked();
104111 const e = switch (@typeInfo(T)) {
105112 .@"struct" => elem,
106113 .@"union" => Elem.fromT(elem),
......@@ -123,25 +130,31 @@ pub fn MultiArrayList(comptime T: type) type {
123130 };
124131 }
125132
133 /// Invalidates pre-existing pointers to fields of the element at indices `a` and `b`.
126134 pub fn swap(self: Slice, a: usize, b: usize) void {
135 self.pointer_stability.assertUnlocked();
127136 inline for (@typeInfo(Field).@"enum".field_names) |field_name| {
128137 const its = self.items(@field(Field, field_name));
129138 std.mem.swap(@FieldType(T, field_name), &its[a], &its[b]);
130139 }
131140 }
132141
142 /// The returned MultiArrayList inherits the locked/unlocked state of this Slice
133143 pub fn toMultiArrayList(self: Slice) Self {
134144 if (self.ptrs.len == 0 or self.capacity == 0) {
135 return .{};
145 return .empty;
136146 }
137147 return .{
138148 .bytes = self.ptrs[sizes.fields[0]],
139149 .len = self.len,
140150 .capacity = self.capacity,
151 .pointer_stability = self.pointer_stability,
141152 };
142153 }
143154
155 /// Invalidates all element pointers
144156 pub fn deinit(self: *Slice, gpa: Allocator) void {
157 self.pointer_stability.assertUnlocked();
145158 var other = self.toMultiArrayList();
146159 other.deinit(gpa);
147160 self.* = undefined;
......@@ -160,9 +173,27 @@ pub fn MultiArrayList(comptime T: type) type {
160173 .ptrs = ptrs,
161174 .len = len,
162175 .capacity = len,
176 .pointer_stability = s.pointer_stability,
163177 };
164178 }
165179
180 /// Puts the Slice into a state where any method call that
181 /// would cause an existing item pointer to become invalidated will
182 /// instead trigger an assertion.
183 ///
184 /// `lockPointers` may be called multiple times. This allows multiple
185 /// independent users of the hash map to keep it locked simultaneously.
186 ///
187 /// `unlockPointers` returns the Slice to the previous state.
188 pub fn lockPointers(self: *Slice) void {
189 self.pointer_stability.lockShared();
190 }
191
192 /// Undoes one call to `lockPointers`.
193 pub fn unlockPointers(self: *Slice) void {
194 self.pointer_stability.unlockShared();
195 }
196
166197 /// This function is used in the debugger pretty formatters in lib/lldb/ to fetch the
167198 /// child field order and entry type to facilitate fancy debug printing for this type.
168199 fn dbHelper(self: *Slice, child: *Elem, field: *Field, entry: *Entry) void {
......@@ -219,12 +250,32 @@ pub fn MultiArrayList(comptime T: type) type {
219250 };
220251
221252 /// Release all allocated memory.
253 /// Invalidates all element pointers.
222254 pub fn deinit(self: *Self, gpa: Allocator) void {
223255 gpa.free(self.allocatedBytes());
224256 self.* = undefined;
225257 }
226258
259 /// Puts the MultiArrayList into a state where any method call that
260 /// would cause an existing item pointer to become invalidated will
261 /// instead trigger an assertion.
262 ///
263 /// `lockPointers` may be called multiple times. This allows multiple
264 /// independent users of the hash map to keep it locked simultaneously.
265 ///
266 /// `unlockPointers` returns the MultiArrayList to the previous state.
267 pub fn lockPointers(self: *Self) void {
268 self.pointer_stability.lockShared();
269 }
270
271 /// Undoes one call to `lockPointers`.
272 pub fn unlockPointers(self: *Self) void {
273 self.pointer_stability.unlockShared();
274 }
275
227276 /// The caller owns the returned memory. Empties this MultiArrayList.
277 /// Does not invalidate element pointers.
278 /// The returned Slice inherits its locked/unlocked state from this MultiArrayList.
228279 pub fn toOwnedSlice(self: *Self) Slice {
229280 const result = self.slice();
230281 self.* = .empty;
......@@ -234,11 +285,14 @@ pub fn MultiArrayList(comptime T: type) type {
234285 /// Compute pointers to the start of each field of the array.
235286 /// If you need to access multiple fields, calling this may
236287 /// be more efficient than calling `items()` multiple times.
288 /// Does not invalidate element pointers.
289 /// The returned Slice inherits its locked/unlocked state from this MultiArrayList.
237290 pub fn slice(self: Self) Slice {
238291 var result: Slice = .{
239292 .ptrs = undefined,
240293 .len = self.len,
241294 .capacity = self.capacity,
295 .pointer_stability = self.pointer_stability,
242296 };
243297 var ptr: [*]u8 = self.bytes;
244298 for (sizes.bytes, sizes.fields) |field_size, i| {
......@@ -251,12 +305,15 @@ pub fn MultiArrayList(comptime T: type) type {
251305 /// Get the slice of values for a specified field.
252306 /// If you need multiple fields, consider calling slice()
253307 /// instead.
308 /// The returned pointer may be invalidated by various operations to this MultiArrayList.
254309 pub fn items(self: Self, comptime field: Field) []FieldType(field) {
255310 return self.slice().items(field);
256311 }
257312
258313 /// Overwrite one array element with new data.
314 /// Invalidates previously existing pointers to fields of that element.
259315 pub fn set(self: *Self, index: usize, elem: T) void {
316 self.pointer_stability.assertUnlocked();
260317 var slices = self.slice();
261318 slices.set(index, elem);
262319 }
......@@ -266,13 +323,17 @@ pub fn MultiArrayList(comptime T: type) type {
266323 return self.slice().get(index);
267324 }
268325
326 /// Invalidates previously existing pointers to fields of the element
327 /// at indices `a` and `b`.
269328 pub fn swap(self: Self, a: usize, b: usize) void {
329 self.pointer_stability.assertUnlocked();
270330 return self.slice().swap(a, b);
271331 }
272332
273333 /// Extend the list by 1 element.
274334 ///
275335 /// Allocates more memory as necessary.
336 /// Invalidates element pointers if more memory is needed.
276337 pub fn append(self: *Self, gpa: Allocator, elem: T) Allocator.Error!void {
277338 try self.ensureUnusedCapacity(gpa, 1);
278339 self.appendAssumeCapacity(elem);
......@@ -281,6 +342,7 @@ pub fn MultiArrayList(comptime T: type) type {
281342 /// Extend the list by 1 element.
282343 ///
283344 /// Asserts that capacity is sufficient to hold an additional item.
345 /// Never invalidates element pointers.
284346 pub fn appendAssumeCapacity(self: *Self, elem: T) void {
285347 assert(self.len < self.capacity);
286348 self.len += 1;
......@@ -291,6 +353,7 @@ pub fn MultiArrayList(comptime T: type) type {
291353 ///
292354 /// If capacity is not sufficient to hold an additional
293355 /// item, returns `error.OutOfMemory`.
356 /// Never invalidates element pointers.
294357 pub fn appendBounded(self: *Self, elem: T) error{OutOfMemory}!void {
295358 if (self.capacity - self.len < 1) return error.OutOfMemory;
296359 return appendAssumeCapacity(self, elem);
......@@ -300,6 +363,7 @@ pub fn MultiArrayList(comptime T: type) type {
300363 /// index with uninitialized data.
301364 ///
302365 /// Allocates more memory as necessary.
366 /// Invalidates element pointers if more memory is needed.
303367 pub fn addOne(self: *Self, gpa: Allocator) Allocator.Error!usize {
304368 try self.ensureUnusedCapacity(gpa, 1);
305369 return self.addOneAssumeCapacity();
......@@ -309,6 +373,7 @@ pub fn MultiArrayList(comptime T: type) type {
309373 /// index with uninitialized data.
310374 ///
311375 /// Asserts that capacity is sufficient to hold an additional item.
376 /// Never invalidates element pointers.
312377 pub fn addOneAssumeCapacity(self: *Self) usize {
313378 assert(self.len < self.capacity);
314379 const index = self.len;
......@@ -321,6 +386,7 @@ pub fn MultiArrayList(comptime T: type) type {
321386 ///
322387 /// If capacity is not sufficient to hold an additional
323388 /// item, returns `error.OutOfMemory`.
389 /// Never invalidates element pointers.
324390 pub fn addOneBounded(self: *Self) error{OutOfMemory}!usize {
325391 if (self.capacity - self.len < 1) return error.OutOfMemory;
326392 return addOneAssumeCapacity(self);
......@@ -329,6 +395,7 @@ pub fn MultiArrayList(comptime T: type) type {
329395 /// Remove and return the last element from the list, or return `null` if list is empty.
330396 /// Invalidates pointers to fields of the removed element.
331397 pub fn pop(self: *Self) ?T {
398 self.pointer_stability.assertUnlocked();
332399 if (self.len == 0) return null;
333400 const val = self.get(self.len - 1);
334401 self.len -= 1;
......@@ -340,6 +407,7 @@ pub fn MultiArrayList(comptime T: type) type {
340407 /// sets the given index to the specified element.
341408 ///
342409 /// Allocates more memory as necessary.
410 /// Invalidates pointers to shifted elements.
343411 pub fn insert(self: *Self, gpa: Allocator, index: usize, elem: T) !void {
344412 try self.ensureUnusedCapacity(gpa, 1);
345413 self.insertAssumeCapacity(index, elem);
......@@ -350,9 +418,11 @@ pub fn MultiArrayList(comptime T: type) type {
350418 /// sets the given index to the specified element.
351419 ///
352420 /// Asserts that capacity is sufficient to hold an additional item.
421 /// Invalidates pointers to shifted elements.
353422 pub fn insertAssumeCapacity(self: *Self, index: usize, elem: T) void {
354423 assert(self.len < self.capacity);
355424 assert(index <= self.len);
425 self.pointer_stability.assertUnlocked();
356426 self.len += 1;
357427 const entry = switch (@typeInfo(T)) {
358428 .@"struct" => elem,
......@@ -376,6 +446,7 @@ pub fn MultiArrayList(comptime T: type) type {
376446 ///
377447 /// If capacity is not sufficient to hold an additional
378448 /// item, returns `error.OutOfMemory`.
449 /// Invalidates pointers to shifted elements.
379450 pub fn insertBounded(self: *Self, index: usize, elem: T) error{OutOfMemory}!void {
380451 if (self.capacity - self.len < 1) return error.OutOfMemory;
381452 return insertAssumeCapacity(self, index, elem);
......@@ -384,7 +455,9 @@ pub fn MultiArrayList(comptime T: type) type {
384455 /// Remove the specified item from the list, swapping the last
385456 /// item in the list into its position. Fast, but does not
386457 /// retain list ordering.
458 /// Invalidates pointers to fields of the item at the end of the list.
387459 pub fn swapRemove(self: *Self, index: usize) void {
460 self.pointer_stability.assertUnlocked();
388461 const slices = self.slice();
389462 inline for (field_names, 0..) |_, i| {
390463 const field_slice = slices.items(@as(Field, @fromBackingInt(@intCast(i))));
......@@ -396,7 +469,9 @@ pub fn MultiArrayList(comptime T: type) type {
396469
397470 /// Remove the specified item from the list, shifting items
398471 /// after it to preserve order.
472 /// Invalidates pointers to shifted elements.
399473 pub fn orderedRemove(self: *Self, index: usize) void {
474 self.pointer_stability.assertUnlocked();
400475 const slices = self.slice();
401476 inline for (field_names, 0..) |_, field_index| {
402477 const field_slice = slices.items(@as(Field, @fromBackingInt(@intCast(field_index))));
......@@ -423,6 +498,7 @@ pub fn MultiArrayList(comptime T: type) type {
423498 /// Invalidates element pointers beyond the first deleted index.
424499 pub fn orderedRemoveMany(self: *Self, sorted_indexes: []const usize) void {
425500 if (sorted_indexes.len == 0) return;
501 self.pointer_stability.assertUnlocked();
426502 const slices = self.slice();
427503 var shift: usize = 1;
428504 for (sorted_indexes[0 .. sorted_indexes.len - 1], sorted_indexes[1..]) |removed, end| {
......@@ -447,6 +523,7 @@ pub fn MultiArrayList(comptime T: type) type {
447523
448524 /// Adjust the list's length to `new_len`.
449525 /// Does not initialize added items, if any.
526 /// Invalidates element pointers if additional memory is required.
450527 pub fn resize(self: *Self, gpa: Allocator, new_len: usize) Allocator.Error!void {
451528 try self.ensureTotalCapacity(gpa, new_len);
452529 self.len = new_len;
......@@ -455,11 +532,12 @@ pub fn MultiArrayList(comptime T: type) type {
455532 /// Attempt to reduce allocated capacity to `new_len`.
456533 /// If `new_len` is greater than zero, this may fail to reduce the capacity,
457534 /// but the data remains intact and the length is updated to new_len.
535 /// Invalidates element pointers if additional memory is required.
458536 pub fn shrinkAndFree(self: *Self, gpa: Allocator, new_len: usize) void {
459537 if (new_len == 0) return clearAndFree(self, gpa);
460
461538 assert(new_len <= self.capacity);
462539 assert(new_len <= self.len);
540 self.pointer_stability.assertUnlocked();
463541
464542 const other_bytes = gpa.alignedAlloc(u8, sizes.big_align, capacityInBytes(new_len)) catch {
465543 const self_slice = self.slice();
......@@ -476,7 +554,7 @@ pub fn MultiArrayList(comptime T: type) type {
476554 self.len = new_len;
477555 return;
478556 };
479 var other = Self{
557 var other: Self = .{
480558 .bytes = other_bytes.ptr,
481559 .capacity = new_len,
482560 .len = new_len,
......@@ -494,7 +572,9 @@ pub fn MultiArrayList(comptime T: type) type {
494572 self.* = other;
495573 }
496574
575 /// Invalidates all element pointers.
497576 pub fn clearAndFree(self: *Self, gpa: Allocator) void {
577 self.pointer_stability.assertUnlocked();
498578 gpa.free(self.allocatedBytes());
499579 self.* = .empty;
500580 }
......@@ -503,11 +583,13 @@ pub fn MultiArrayList(comptime T: type) type {
503583 /// Invalidates pointers to elements `items[new_len..]`.
504584 /// Keeps capacity the same.
505585 pub fn shrinkRetainingCapacity(self: *Self, new_len: usize) void {
586 self.pointer_stability.assertUnlocked();
506587 self.len = new_len;
507588 }
508589
509590 /// Invalidates all element pointers.
510591 pub fn clearRetainingCapacity(self: *Self) void {
592 self.pointer_stability.assertUnlocked();
511593 self.len = 0;
512594 }
513595
......@@ -542,6 +624,7 @@ pub fn MultiArrayList(comptime T: type) type {
542624 /// `new_capacity` must be greater or equal to `len`.
543625 pub fn setCapacity(self: *Self, gpa: Allocator, new_capacity: usize) Allocator.Error!void {
544626 assert(new_capacity >= self.len);
627 self.pointer_stability.assertUnlocked();
545628 const new_bytes = try gpa.alignedAlloc(u8, sizes.big_align, capacityInBytes(new_capacity));
546629 if (self.len == 0) {
547630 gpa.free(self.allocatedBytes());
......@@ -549,7 +632,7 @@ pub fn MultiArrayList(comptime T: type) type {
549632 self.capacity = new_capacity;
550633 return;
551634 }
552 var other = Self{
635 var other: Self = .{
553636 .bytes = new_bytes.ptr,
554637 .capacity = new_capacity,
555638 .len = self.len,
......@@ -568,8 +651,9 @@ pub fn MultiArrayList(comptime T: type) type {
568651
569652 /// Create a copy of this list with a new backing store,
570653 /// using the specified allocator.
654 /// The returned MultiArrayList is in an unlocked state.
571655 pub fn clone(self: Self, gpa: Allocator) Allocator.Error!Self {
572 var result = Self{};
656 var result: Self = .{};
573657 errdefer result.deinit(gpa);
574658 try result.ensureTotalCapacity(gpa, self.len);
575659 result.len = self.len;
......@@ -584,9 +668,11 @@ pub fn MultiArrayList(comptime T: type) type {
584668 return result;
585669 }
586670
671 /// Invalidates element pointers.
587672 /// `ctx` has the following method:
588673 /// `fn lessThan(ctx: @TypeOf(ctx), a_index: usize, b_index: usize) bool`
589674 fn sortInternal(self: Self, a: usize, b: usize, ctx: anytype, comptime mode: std.sort.Mode) void {
675 self.pointer_stability.assertUnlocked();
590676 const sort_context: struct {
591677 sub_ctx: @TypeOf(ctx),
592678 slice: Slice,
......@@ -616,6 +702,7 @@ pub fn MultiArrayList(comptime T: type) type {
616702 }
617703
618704 /// This function guarantees a stable sort, i.e the relative order of equal elements is preserved during sorting.
705 /// Invalidates element pointers.
619706 /// Read more about stable sorting here: https://en.wikipedia.org/wiki/Sorting_algorithm#Stability
620707 /// If this guarantee does not matter, `sortUnstable` might be a faster alternative.
621708 /// `ctx` has the following method:
......@@ -625,6 +712,7 @@ pub fn MultiArrayList(comptime T: type) type {
625712 }
626713
627714 /// Sorts only the subsection of items between indices `a` and `b` (excluding `b`)
715 /// Invalidates element pointers.
628716 /// This function guarantees a stable sort, i.e the relative order of equal elements is preserved during sorting.
629717 /// Read more about stable sorting here: https://en.wikipedia.org/wiki/Sorting_algorithm#Stability
630718 /// If this guarantee does not matter, `sortSpanUnstable` might be a faster alternative.
......@@ -635,6 +723,7 @@ pub fn MultiArrayList(comptime T: type) type {
635723 }
636724
637725 /// This function does NOT guarantee a stable sort, i.e the relative order of equal elements may change during sorting.
726 /// Invalidates element pointers.
638727 /// Due to the weaker guarantees of this function, this may be faster than the stable `sort` method.
639728 /// Read more about stable sorting here: https://en.wikipedia.org/wiki/Sorting_algorithm#Stability
640729 /// `ctx` has the following method:
......@@ -644,6 +733,7 @@ pub fn MultiArrayList(comptime T: type) type {
644733 }
645734
646735 /// Sorts only the subsection of items between indices `a` and `b` (excluding `b`)
736 /// Invalidates element pointers.
647737 /// This function does NOT guarantee a stable sort, i.e the relative order of equal elements may change during sorting.
648738 /// Due to the weaker guarantees of this function, this may be faster than the stable `sortSpan` method.
649739 /// Read more about stable sorting here: https://en.wikipedia.org/wiki/Sorting_algorithm#Stability
lib/std/zig/TokenSmith.zig+1
......@@ -246,6 +246,7 @@ pub fn list(t: *TokenSmith) TokenList.Slice {
246246 .ptrs = undefined,
247247 .len = t.tags_len,
248248 .capacity = t.tags_len,
249 .pointer_stability = .{},
249250 };
250251 comptime std.debug.assert(slice.ptrs.len == 2);
251252 slice.ptrs[@backingInt(TokenList.Field.tag)] = @ptrCast(&t.tag_buf);