-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Open
Labels
bugObserved behavior contradicts documented or intended behaviorObserved behavior contradicts documented or intended behaviorcontributor friendlyThis issue is limited in scope and/or knowledge of Zig internals.This issue is limited in scope and/or knowledge of Zig internals.standard libraryThis issue involves writing Zig code for the standard library.This issue involves writing Zig code for the standard library.
Milestone
Description
Zig Version
0.14.0-dev.2613+0bf44c309
Description
Calling ArrayList.toOwnedSlice()
with a zero-sized type triggers an assertion error in the allocator's resize implementation when using certain allocators.
Steps to Reproduce
Minimal Example
const std = @import("std");
const Foo = struct {};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var arr = std.ArrayList(Foo).init(allocator);
try arr.append(.{});
try arr.append(.{});
const slice = try arr.toOwnedSlice(); // assertion error occurs here
std.debug.print("{any}\n", .{slice});
}
Observed Behavior
The program panics with an assertion error when calling toOwnedSlice()
.
Stack Trace
thread 1 panic: reached unreachable code
/usr/lib/zig/std/debug.zig:403:14: 0x103987d in assert (main)
if (!ok) unreachable; // assertion failure
^
/usr/lib/zig/std/heap/general_purpose_allocator.zig:709:19: 0x103a4a9 in resize (main)
assert(old_mem.len != 0);
^
/usr/lib/zig/std/mem/Allocator.zig:92:30: 0x103c608 in resize__anon_4164 (main)
return self.vtable.resize(self.ptr, buf, log2_buf_align, new_len, ret_addr);
^
/usr/lib/zig/std/array_list.zig:114:33: 0x1036ba1 in toOwnedSlice (main)
if (allocator.resize(old_memory, self.items.len)) {
^
/sandbox/src/main.zig:12:43: 0x10367db in main (main)
const slice = try arr.toOwnedSlice();
Allocator-Specific Behavior
- Fails with assertion error:
std.heap.GeneralPurposeAllocator
std.heap.FixedBufferAllocator
- Works correctly:
std.heap.page_allocator
std.heap.ArenaAllocator
(with any underlying allocator including GPA, page_allocator, and FixedBufferAllocator)
Expected Behavior
The program should complete without any assertion errors and print:
{ main.Foo{ }, main.Foo{ } }
Technical Analysis
Zero-Sized Type Behavior
- The size of
Foo
(@sizeOf(Foo)
) is 0 - Slices of
Foo
have a conceptual length but occupy no actual memory, which leads to mismatch described below:
Implementation Details
toOwnedSlice
callsallocator.resize(old_memory, self.items.len)
to attempt an in-place resize- Inside
Allocator.resize
,mem.sliceAsBytes
is called to convert the slice to bytes:const old_byte_slice = mem.sliceAsBytes(old_mem);
- The
sliceAsBytes
function has special handling for zero-sized types:This converts any slice of zero-sized types into an empty byte slice, regardless of the original slice's lengthif (@sizeOf(std.meta.Elem(Slice)) == 0) return &[0]u8{};
- The empty byte slice is passed through
Allocator.rawResize
to the allocator's vtable implementation - In the allocator's resize implementation, there's an assertion:
This assertion fails because the byte slice length is 0, even though the original slice had a non-zero length
fn resize(...) bool { ... assert(old_mem.len != 0); // Fails because old_mem is now empty
Root Cause
std.ArrayList.toOwnedSlice
does not account for zero-sized types correctly, resulting in an invalid call to allocator.resize
. The mismatch between the conceptual length of the zero-sized type slice and the actual byte slice length leads to the assertion failure.
Proposed solution
Since zero-sized types require no actual memory allocation, we can short-circuit the resize operation for them. Here's my proposed modification to toOwnedSlice:
pub fn toOwnedSlice(self: *Self) Allocator.Error!Slice {
const allocator = self.allocator;
if (@sizeOf(T) == 0 or allocator.resize(self.allocatedSlice(), self.items.len)) {
const result = self.items;
self.* = init(allocator);
return result;
}
const new_memory = try allocator.alignedAlloc(T, alignment, self.items.len);
@memcpy(new_memory, self.items);
@memset(self.items, undefined);
self.clearAndFree();
return new_memory;
}
This:
- Short-circuits evaluation to handle both zero-sized types and successful resize operations
- Maintains the existing behavior for types that require memory allocation
- Preserves the contract that the ArrayList is emptied after the call
- Avoids the problematic resize call entirely for zero-sized types
Metadata
Metadata
Assignees
Labels
bugObserved behavior contradicts documented or intended behaviorObserved behavior contradicts documented or intended behaviorcontributor friendlyThis issue is limited in scope and/or knowledge of Zig internals.This issue is limited in scope and/or knowledge of Zig internals.standard libraryThis issue involves writing Zig code for the standard library.This issue involves writing Zig code for the standard library.