zig/lib/std/multi_array_list.zig
mlugg 4d7818a76a
compiler: allow files with AstGen errors to undergo semantic analysis
This commit enhances AstGen to introduce a form of error resilience
which allows valid ZIR to be emitted even when AstGen errors occur.

When a non-fatal AstGen error (e.g. `appendErrorNode`) occurs, ZIR
generation is not affected; the error is added to `astgen.errors` and
ultimately to the errors stored in `extra`, but that doesn't stop us
getting valid ZIR. Fatal AstGen errors (e.g. `failNode`) are a bit
trickier. These errors return `error.AnalysisFail`, which is propagated
up the stack. In theory, any parent expression can catch this error and
handle it, continuing ZIR generation whilst throwing away whatever was
lost. For now, we only do this in one place: when creating declarations.
If a call to `fnDecl`, `comptimeDecl`, `globalVarDecl`, etc, returns
`error.AnalysisFail`, the `declaration` instruction is still created,
but its body simply contains the new `extended(astgen_error())`
instruction, which instructs Sema to terminate semantic analysis with a
transitive error. This means that a fatal AstGen error causes the
innermost declaration containing the error to fail, but the rest of the
file remains intact.

If a source file contains parse errors, or an `error.AnalysisFail`
happens when lowering the top-level struct (e.g. there is an error in
one of its fields, or a name has multiple declarations), then lowering
for the entire file fails. Alongside the existing `Zir.hasCompileErrors`
query, this commit introduces `Zir.loweringFailed`, which returns `true`
only in this case.

The end result here is that files with AstGen failures will almost
always still emit valid ZIR, and hence can undergo semantic analysis on
the parts of the file which are (from AstGen's perspective) valid. This
is a noteworthy improvement to UX, but the main motivation here is
actually incremental compilation. Previously, AstGen failures caused
lots of semantic analysis work to be thrown out, because all `AnalUnit`s
in the file required re-analysis so as to trigger necessary transitive
failures and remove stored compile errors which would no longer make
sense (because a fresh compilation of this code would not emit those
errors, as the units those errors applied to would fail sooner due to
referencing a failed file). Now, this case only applies when a file has
severe top-level errors, which is far less common than something like
having an unused variable.

Lastly, this commit changes a few errors in `AstGen` to become fatal
when they were previously non-fatal and vice versa. If there is still a
reasonable way to continue AstGen and lower to ZIR after an error, it is
non-fatal; otherwise, it is fatal. For instance, `comptime const`, while
redundant syntax, has a clear meaning we can lower; on the other hand,
using an undeclared identifer has no sane lowering, so must trigger a
fatal error.
2024-12-05 19:58:38 +00:00

979 lines
38 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const meta = std.meta;
const mem = std.mem;
const Allocator = mem.Allocator;
const testing = std.testing;
/// A MultiArrayList stores a list of a struct or tagged union type.
/// Instead of storing a single list of items, MultiArrayList
/// stores separate lists for each field of the struct or
/// lists of tags and bare unions.
/// This allows for memory savings if the struct or union has padding,
/// and also improves cache usage if only some fields or just tags
/// are needed for a computation. The primary API for accessing fields is
/// the `slice()` function, which computes the start pointers
/// for the array of each field. From the slice you can call
/// `.items(.<field_name>)` to obtain a slice of field values.
/// For unions you can call `.items(.tags)` or `.items(.data)`.
pub fn MultiArrayList(comptime T: type) type {
return struct {
bytes: [*]align(@alignOf(T)) u8 = undefined,
len: usize = 0,
capacity: usize = 0,
pub const empty: Self = .{
.bytes = undefined,
.len = 0,
.capacity = 0,
};
const Elem = switch (@typeInfo(T)) {
.@"struct" => T,
.@"union" => |u| struct {
pub const Bare = @Type(.{ .@"union" = .{
.layout = u.layout,
.tag_type = null,
.fields = u.fields,
.decls = &.{},
} });
pub const Tag =
u.tag_type orelse @compileError("MultiArrayList does not support untagged unions");
tags: Tag,
data: Bare,
pub fn fromT(outer: T) @This() {
const tag = meta.activeTag(outer);
return .{
.tags = tag,
.data = switch (tag) {
inline else => |t| @unionInit(Bare, @tagName(t), @field(outer, @tagName(t))),
},
};
}
pub fn toT(tag: Tag, bare: Bare) T {
return switch (tag) {
inline else => |t| @unionInit(T, @tagName(t), @field(bare, @tagName(t))),
};
}
},
else => @compileError("MultiArrayList only supports structs and tagged unions"),
};
pub const Field = meta.FieldEnum(Elem);
/// A MultiArrayList.Slice contains cached start pointers for each field in the list.
/// These pointers are not normally stored to reduce the size of the list in memory.
/// If you are accessing multiple fields, call slice() first to compute the pointers,
/// and then get the field arrays from the slice.
pub const Slice = struct {
/// This array is indexed by the field index which can be obtained
/// by using @intFromEnum() on the Field enum
ptrs: [fields.len][*]u8,
len: usize,
capacity: usize,
pub const empty: Slice = .{
.ptrs = undefined,
.len = 0,
.capacity = 0,
};
pub fn items(self: Slice, comptime field: Field) []FieldType(field) {
const F = FieldType(field);
if (self.capacity == 0) {
return &[_]F{};
}
const byte_ptr = self.ptrs[@intFromEnum(field)];
const casted_ptr: [*]F = if (@sizeOf(F) == 0)
undefined
else
@ptrCast(@alignCast(byte_ptr));
return casted_ptr[0..self.len];
}
pub fn set(self: *Slice, index: usize, elem: T) void {
const e = switch (@typeInfo(T)) {
.@"struct" => elem,
.@"union" => Elem.fromT(elem),
else => unreachable,
};
inline for (fields, 0..) |field_info, i| {
self.items(@as(Field, @enumFromInt(i)))[index] = @field(e, field_info.name);
}
}
pub fn get(self: Slice, index: usize) T {
var result: Elem = undefined;
inline for (fields, 0..) |field_info, i| {
@field(result, field_info.name) = self.items(@as(Field, @enumFromInt(i)))[index];
}
return switch (@typeInfo(T)) {
.@"struct" => result,
.@"union" => Elem.toT(result.tags, result.data),
else => unreachable,
};
}
pub fn toMultiArrayList(self: Slice) Self {
if (self.ptrs.len == 0 or self.capacity == 0) {
return .{};
}
const unaligned_ptr = self.ptrs[sizes.fields[0]];
const aligned_ptr: [*]align(@alignOf(Elem)) u8 = @alignCast(unaligned_ptr);
return .{
.bytes = aligned_ptr,
.len = self.len,
.capacity = self.capacity,
};
}
pub fn deinit(self: *Slice, gpa: Allocator) void {
var other = self.toMultiArrayList();
other.deinit(gpa);
self.* = undefined;
}
/// This function is used in the debugger pretty formatters in tools/ to fetch the
/// child field order and entry type to facilitate fancy debug printing for this type.
fn dbHelper(self: *Slice, child: *Elem, field: *Field, entry: *Entry) void {
_ = self;
_ = child;
_ = field;
_ = entry;
}
};
const Self = @This();
const fields = meta.fields(Elem);
/// `sizes.bytes` is an array of @sizeOf each T field. Sorted by alignment, descending.
/// `sizes.fields` is an array mapping from `sizes.bytes` array index to field index.
const sizes = blk: {
const Data = struct {
size: usize,
size_index: usize,
alignment: usize,
};
var data: [fields.len]Data = undefined;
for (fields, 0..) |field_info, i| {
data[i] = .{
.size = @sizeOf(field_info.type),
.size_index = i,
.alignment = if (@sizeOf(field_info.type) == 0) 1 else field_info.alignment,
};
}
const Sort = struct {
fn lessThan(context: void, lhs: Data, rhs: Data) bool {
_ = context;
return lhs.alignment > rhs.alignment;
}
};
mem.sort(Data, &data, {}, Sort.lessThan);
var sizes_bytes: [fields.len]usize = undefined;
var field_indexes: [fields.len]usize = undefined;
for (data, 0..) |elem, i| {
sizes_bytes[i] = elem.size;
field_indexes[i] = elem.size_index;
}
break :blk .{
.bytes = sizes_bytes,
.fields = field_indexes,
};
};
/// Release all allocated memory.
pub fn deinit(self: *Self, gpa: Allocator) void {
gpa.free(self.allocatedBytes());
self.* = undefined;
}
/// The caller owns the returned memory. Empties this MultiArrayList.
pub fn toOwnedSlice(self: *Self) Slice {
const result = self.slice();
self.* = .{};
return result;
}
/// Compute pointers to the start of each field of the array.
/// If you need to access multiple fields, calling this may
/// be more efficient than calling `items()` multiple times.
pub fn slice(self: Self) Slice {
var result: Slice = .{
.ptrs = undefined,
.len = self.len,
.capacity = self.capacity,
};
var ptr: [*]u8 = self.bytes;
for (sizes.bytes, sizes.fields) |field_size, i| {
result.ptrs[i] = ptr;
ptr += field_size * self.capacity;
}
return result;
}
/// Get the slice of values for a specified field.
/// If you need multiple fields, consider calling slice()
/// instead.
pub fn items(self: Self, comptime field: Field) []FieldType(field) {
return self.slice().items(field);
}
/// Overwrite one array element with new data.
pub fn set(self: *Self, index: usize, elem: T) void {
var slices = self.slice();
slices.set(index, elem);
}
/// Obtain all the data for one array element.
pub fn get(self: Self, index: usize) T {
return self.slice().get(index);
}
/// Extend the list by 1 element. Allocates more memory as necessary.
pub fn append(self: *Self, gpa: Allocator, elem: T) !void {
try self.ensureUnusedCapacity(gpa, 1);
self.appendAssumeCapacity(elem);
}
/// Extend the list by 1 element, but asserting `self.capacity`
/// is sufficient to hold an additional item.
pub fn appendAssumeCapacity(self: *Self, elem: T) void {
assert(self.len < self.capacity);
self.len += 1;
self.set(self.len - 1, elem);
}
/// Extend the list by 1 element, returning the newly reserved
/// index with uninitialized data.
/// Allocates more memory as necesasry.
pub fn addOne(self: *Self, allocator: Allocator) Allocator.Error!usize {
try self.ensureUnusedCapacity(allocator, 1);
return self.addOneAssumeCapacity();
}
/// Extend the list by 1 element, asserting `self.capacity`
/// is sufficient to hold an additional item. Returns the
/// newly reserved index with uninitialized data.
pub fn addOneAssumeCapacity(self: *Self) usize {
assert(self.len < self.capacity);
const index = self.len;
self.len += 1;
return index;
}
/// Remove and return the last element from the list.
/// Asserts the list has at least one item.
/// Invalidates pointers to fields of the removed element.
pub fn pop(self: *Self) T {
const val = self.get(self.len - 1);
self.len -= 1;
return val;
}
/// Remove and return the last element from the list, or
/// return `null` if list is empty.
/// Invalidates pointers to fields of the removed element, if any.
pub fn popOrNull(self: *Self) ?T {
if (self.len == 0) return null;
return self.pop();
}
/// Inserts an item into an ordered list. Shifts all elements
/// after and including the specified index back by one and
/// sets the given index to the specified element. May reallocate
/// and invalidate iterators.
pub fn insert(self: *Self, gpa: Allocator, index: usize, elem: T) !void {
try self.ensureUnusedCapacity(gpa, 1);
self.insertAssumeCapacity(index, elem);
}
/// Inserts an item into an ordered list which has room for it.
/// Shifts all elements after and including the specified index
/// back by one and sets the given index to the specified element.
/// Will not reallocate the array, does not invalidate iterators.
pub fn insertAssumeCapacity(self: *Self, index: usize, elem: T) void {
assert(self.len < self.capacity);
assert(index <= self.len);
self.len += 1;
const entry = switch (@typeInfo(T)) {
.@"struct" => elem,
.@"union" => Elem.fromT(elem),
else => unreachable,
};
const slices = self.slice();
inline for (fields, 0..) |field_info, field_index| {
const field_slice = slices.items(@as(Field, @enumFromInt(field_index)));
var i: usize = self.len - 1;
while (i > index) : (i -= 1) {
field_slice[i] = field_slice[i - 1];
}
field_slice[index] = @field(entry, field_info.name);
}
}
/// Remove the specified item from the list, swapping the last
/// item in the list into its position. Fast, but does not
/// retain list ordering.
pub fn swapRemove(self: *Self, index: usize) void {
const slices = self.slice();
inline for (fields, 0..) |_, i| {
const field_slice = slices.items(@as(Field, @enumFromInt(i)));
field_slice[index] = field_slice[self.len - 1];
field_slice[self.len - 1] = undefined;
}
self.len -= 1;
}
/// Remove the specified item from the list, shifting items
/// after it to preserve order.
pub fn orderedRemove(self: *Self, index: usize) void {
const slices = self.slice();
inline for (fields, 0..) |_, field_index| {
const field_slice = slices.items(@as(Field, @enumFromInt(field_index)));
var i = index;
while (i < self.len - 1) : (i += 1) {
field_slice[i] = field_slice[i + 1];
}
field_slice[i] = undefined;
}
self.len -= 1;
}
/// Adjust the list's length to `new_len`.
/// Does not initialize added items, if any.
pub fn resize(self: *Self, gpa: Allocator, new_len: usize) !void {
try self.ensureTotalCapacity(gpa, new_len);
self.len = new_len;
}
/// Attempt to reduce allocated capacity to `new_len`.
/// If `new_len` is greater than zero, this may fail to reduce the capacity,
/// but the data remains intact and the length is updated to new_len.
pub fn shrinkAndFree(self: *Self, gpa: Allocator, new_len: usize) void {
if (new_len == 0) return clearAndFree(self, gpa);
assert(new_len <= self.capacity);
assert(new_len <= self.len);
const other_bytes = gpa.alignedAlloc(
u8,
@alignOf(Elem),
capacityInBytes(new_len),
) catch {
const self_slice = self.slice();
inline for (fields, 0..) |field_info, i| {
if (@sizeOf(field_info.type) != 0) {
const field = @as(Field, @enumFromInt(i));
const dest_slice = self_slice.items(field)[new_len..];
// We use memset here for more efficient codegen in safety-checked,
// valgrind-enabled builds. Otherwise the valgrind client request
// will be repeated for every element.
@memset(dest_slice, undefined);
}
}
self.len = new_len;
return;
};
var other = Self{
.bytes = other_bytes.ptr,
.capacity = new_len,
.len = new_len,
};
self.len = new_len;
const self_slice = self.slice();
const other_slice = other.slice();
inline for (fields, 0..) |field_info, i| {
if (@sizeOf(field_info.type) != 0) {
const field = @as(Field, @enumFromInt(i));
@memcpy(other_slice.items(field), self_slice.items(field));
}
}
gpa.free(self.allocatedBytes());
self.* = other;
}
pub fn clearAndFree(self: *Self, gpa: Allocator) void {
gpa.free(self.allocatedBytes());
self.* = .{};
}
/// Reduce length to `new_len`.
/// Invalidates pointers to elements `items[new_len..]`.
/// Keeps capacity the same.
pub fn shrinkRetainingCapacity(self: *Self, new_len: usize) void {
self.len = new_len;
}
/// Invalidates all element pointers.
pub fn clearRetainingCapacity(self: *Self) void {
self.len = 0;
}
/// Modify the array so that it can hold at least `new_capacity` items.
/// Implements super-linear growth to achieve amortized O(1) append operations.
/// Invalidates pointers if additional memory is needed.
pub fn ensureTotalCapacity(self: *Self, gpa: Allocator, new_capacity: usize) !void {
var better_capacity = self.capacity;
if (better_capacity >= new_capacity) return;
while (true) {
better_capacity += better_capacity / 2 + 8;
if (better_capacity >= new_capacity) break;
}
return self.setCapacity(gpa, better_capacity);
}
/// Modify the array so that it can hold at least `additional_count` **more** items.
/// Invalidates pointers if additional memory is needed.
pub fn ensureUnusedCapacity(self: *Self, gpa: Allocator, additional_count: usize) !void {
return self.ensureTotalCapacity(gpa, self.len + additional_count);
}
/// Modify the array so that it can hold exactly `new_capacity` items.
/// Invalidates pointers if additional memory is needed.
/// `new_capacity` must be greater or equal to `len`.
pub fn setCapacity(self: *Self, gpa: Allocator, new_capacity: usize) !void {
assert(new_capacity >= self.len);
const new_bytes = try gpa.alignedAlloc(
u8,
@alignOf(Elem),
capacityInBytes(new_capacity),
);
if (self.len == 0) {
gpa.free(self.allocatedBytes());
self.bytes = new_bytes.ptr;
self.capacity = new_capacity;
return;
}
var other = Self{
.bytes = new_bytes.ptr,
.capacity = new_capacity,
.len = self.len,
};
const self_slice = self.slice();
const other_slice = other.slice();
inline for (fields, 0..) |field_info, i| {
if (@sizeOf(field_info.type) != 0) {
const field = @as(Field, @enumFromInt(i));
@memcpy(other_slice.items(field), self_slice.items(field));
}
}
gpa.free(self.allocatedBytes());
self.* = other;
}
/// Create a copy of this list with a new backing store,
/// using the specified allocator.
pub fn clone(self: Self, gpa: Allocator) !Self {
var result = Self{};
errdefer result.deinit(gpa);
try result.ensureTotalCapacity(gpa, self.len);
result.len = self.len;
const self_slice = self.slice();
const result_slice = result.slice();
inline for (fields, 0..) |field_info, i| {
if (@sizeOf(field_info.type) != 0) {
const field = @as(Field, @enumFromInt(i));
@memcpy(result_slice.items(field), self_slice.items(field));
}
}
return result;
}
/// `ctx` has the following method:
/// `fn lessThan(ctx: @TypeOf(ctx), a_index: usize, b_index: usize) bool`
fn sortInternal(self: Self, a: usize, b: usize, ctx: anytype, comptime mode: std.sort.Mode) void {
const sort_context: struct {
sub_ctx: @TypeOf(ctx),
slice: Slice,
pub fn swap(sc: @This(), a_index: usize, b_index: usize) void {
inline for (fields, 0..) |field_info, i| {
if (@sizeOf(field_info.type) != 0) {
const field: Field = @enumFromInt(i);
const ptr = sc.slice.items(field);
mem.swap(field_info.type, &ptr[a_index], &ptr[b_index]);
}
}
}
pub fn lessThan(sc: @This(), a_index: usize, b_index: usize) bool {
return sc.sub_ctx.lessThan(a_index, b_index);
}
} = .{
.sub_ctx = ctx,
.slice = self.slice(),
};
switch (mode) {
.stable => mem.sortContext(a, b, sort_context),
.unstable => mem.sortUnstableContext(a, b, sort_context),
}
}
/// This function guarantees a stable sort, i.e the relative order of equal elements is preserved during sorting.
/// Read more about stable sorting here: https://en.wikipedia.org/wiki/Sorting_algorithm#Stability
/// If this guarantee does not matter, `sortUnstable` might be a faster alternative.
/// `ctx` has the following method:
/// `fn lessThan(ctx: @TypeOf(ctx), a_index: usize, b_index: usize) bool`
pub fn sort(self: Self, ctx: anytype) void {
self.sortInternal(0, self.len, ctx, .stable);
}
/// Sorts only the subsection of items between indices `a` and `b` (excluding `b`)
/// This function guarantees a stable sort, i.e the relative order of equal elements is preserved during sorting.
/// Read more about stable sorting here: https://en.wikipedia.org/wiki/Sorting_algorithm#Stability
/// If this guarantee does not matter, `sortSpanUnstable` might be a faster alternative.
/// `ctx` has the following method:
/// `fn lessThan(ctx: @TypeOf(ctx), a_index: usize, b_index: usize) bool`
pub fn sortSpan(self: Self, a: usize, b: usize, ctx: anytype) void {
self.sortInternal(a, b, ctx, .stable);
}
/// This function does NOT guarantee a stable sort, i.e the relative order of equal elements may change during sorting.
/// Due to the weaker guarantees of this function, this may be faster than the stable `sort` method.
/// Read more about stable sorting here: https://en.wikipedia.org/wiki/Sorting_algorithm#Stability
/// `ctx` has the following method:
/// `fn lessThan(ctx: @TypeOf(ctx), a_index: usize, b_index: usize) bool`
pub fn sortUnstable(self: Self, ctx: anytype) void {
self.sortInternal(0, self.len, ctx, .unstable);
}
/// Sorts only the subsection of items between indices `a` and `b` (excluding `b`)
/// This function does NOT guarantee a stable sort, i.e the relative order of equal elements may change during sorting.
/// Due to the weaker guarantees of this function, this may be faster than the stable `sortSpan` method.
/// Read more about stable sorting here: https://en.wikipedia.org/wiki/Sorting_algorithm#Stability
/// `ctx` has the following method:
/// `fn lessThan(ctx: @TypeOf(ctx), a_index: usize, b_index: usize) bool`
pub fn sortSpanUnstable(self: Self, a: usize, b: usize, ctx: anytype) void {
self.sortInternal(a, b, ctx, .unstable);
}
pub fn capacityInBytes(capacity: usize) usize {
comptime var elem_bytes: usize = 0;
inline for (sizes.bytes) |size| elem_bytes += size;
return elem_bytes * capacity;
}
fn allocatedBytes(self: Self) []align(@alignOf(Elem)) u8 {
return self.bytes[0..capacityInBytes(self.capacity)];
}
fn FieldType(comptime field: Field) type {
return meta.fieldInfo(Elem, field).type;
}
const Entry = entry: {
var entry_fields: [fields.len]std.builtin.Type.StructField = undefined;
for (&entry_fields, sizes.fields) |*entry_field, i| entry_field.* = .{
.name = fields[i].name ++ "_ptr",
.type = *fields[i].type,
.default_value = null,
.is_comptime = fields[i].is_comptime,
.alignment = fields[i].alignment,
};
break :entry @Type(.{ .@"struct" = .{
.layout = .@"extern",
.fields = &entry_fields,
.decls = &.{},
.is_tuple = false,
} });
};
/// This function is used in the debugger pretty formatters in tools/ to fetch the
/// child field order and entry type to facilitate fancy debug printing for this type.
fn dbHelper(self: *Self, child: *Elem, field: *Field, entry: *Entry) void {
_ = self;
_ = child;
_ = field;
_ = entry;
}
comptime {
if (builtin.zig_backend == .stage2_llvm and !builtin.strip_debug_info) {
_ = &dbHelper;
_ = &Slice.dbHelper;
}
}
};
}
test "basic usage" {
const ally = testing.allocator;
const Foo = struct {
a: u32,
b: []const u8,
c: u8,
};
var list = MultiArrayList(Foo){};
defer list.deinit(ally);
try testing.expectEqual(@as(usize, 0), list.items(.a).len);
try list.ensureTotalCapacity(ally, 2);
list.appendAssumeCapacity(.{
.a = 1,
.b = "foobar",
.c = 'a',
});
list.appendAssumeCapacity(.{
.a = 2,
.b = "zigzag",
.c = 'b',
});
try testing.expectEqualSlices(u32, list.items(.a), &[_]u32{ 1, 2 });
try testing.expectEqualSlices(u8, list.items(.c), &[_]u8{ 'a', 'b' });
try testing.expectEqual(@as(usize, 2), list.items(.b).len);
try testing.expectEqualStrings("foobar", list.items(.b)[0]);
try testing.expectEqualStrings("zigzag", list.items(.b)[1]);
try list.append(ally, .{
.a = 3,
.b = "fizzbuzz",
.c = 'c',
});
try testing.expectEqualSlices(u32, list.items(.a), &[_]u32{ 1, 2, 3 });
try testing.expectEqualSlices(u8, list.items(.c), &[_]u8{ 'a', 'b', 'c' });
try testing.expectEqual(@as(usize, 3), list.items(.b).len);
try testing.expectEqualStrings("foobar", list.items(.b)[0]);
try testing.expectEqualStrings("zigzag", list.items(.b)[1]);
try testing.expectEqualStrings("fizzbuzz", list.items(.b)[2]);
// Add 6 more things to force a capacity increase.
var i: usize = 0;
while (i < 6) : (i += 1) {
try list.append(ally, .{
.a = @as(u32, @intCast(4 + i)),
.b = "whatever",
.c = @as(u8, @intCast('d' + i)),
});
}
try testing.expectEqualSlices(
u32,
&[_]u32{ 1, 2, 3, 4, 5, 6, 7, 8, 9 },
list.items(.a),
);
try testing.expectEqualSlices(
u8,
&[_]u8{ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i' },
list.items(.c),
);
list.shrinkAndFree(ally, 3);
try testing.expectEqualSlices(u32, list.items(.a), &[_]u32{ 1, 2, 3 });
try testing.expectEqualSlices(u8, list.items(.c), &[_]u8{ 'a', 'b', 'c' });
try testing.expectEqual(@as(usize, 3), list.items(.b).len);
try testing.expectEqualStrings("foobar", list.items(.b)[0]);
try testing.expectEqualStrings("zigzag", list.items(.b)[1]);
try testing.expectEqualStrings("fizzbuzz", list.items(.b)[2]);
list.set(try list.addOne(ally), .{
.a = 4,
.b = "xnopyt",
.c = 'd',
});
try testing.expectEqualStrings("xnopyt", list.pop().b);
try testing.expectEqual(@as(?u8, 'c'), if (list.popOrNull()) |elem| elem.c else null);
try testing.expectEqual(@as(u32, 2), list.pop().a);
try testing.expectEqual(@as(u8, 'a'), list.pop().c);
try testing.expectEqual(@as(?Foo, null), list.popOrNull());
list.clearRetainingCapacity();
try testing.expectEqual(0, list.len);
try testing.expect(list.capacity > 0);
list.clearAndFree(ally);
try testing.expectEqual(0, list.len);
try testing.expectEqual(0, list.capacity);
}
// This was observed to fail on aarch64 with LLVM 11, when the capacityInBytes
// function used the @reduce code path.
test "regression test for @reduce bug" {
const ally = testing.allocator;
var list = MultiArrayList(struct {
tag: std.zig.Token.Tag,
start: u32,
}){};
defer list.deinit(ally);
try list.ensureTotalCapacity(ally, 20);
try list.append(ally, .{ .tag = .keyword_const, .start = 0 });
try list.append(ally, .{ .tag = .identifier, .start = 6 });
try list.append(ally, .{ .tag = .equal, .start = 10 });
try list.append(ally, .{ .tag = .builtin, .start = 12 });
try list.append(ally, .{ .tag = .l_paren, .start = 19 });
try list.append(ally, .{ .tag = .string_literal, .start = 20 });
try list.append(ally, .{ .tag = .r_paren, .start = 25 });
try list.append(ally, .{ .tag = .semicolon, .start = 26 });
try list.append(ally, .{ .tag = .keyword_pub, .start = 29 });
try list.append(ally, .{ .tag = .keyword_fn, .start = 33 });
try list.append(ally, .{ .tag = .identifier, .start = 36 });
try list.append(ally, .{ .tag = .l_paren, .start = 40 });
try list.append(ally, .{ .tag = .r_paren, .start = 41 });
try list.append(ally, .{ .tag = .identifier, .start = 43 });
try list.append(ally, .{ .tag = .bang, .start = 51 });
try list.append(ally, .{ .tag = .identifier, .start = 52 });
try list.append(ally, .{ .tag = .l_brace, .start = 57 });
try list.append(ally, .{ .tag = .identifier, .start = 63 });
try list.append(ally, .{ .tag = .period, .start = 66 });
try list.append(ally, .{ .tag = .identifier, .start = 67 });
try list.append(ally, .{ .tag = .period, .start = 70 });
try list.append(ally, .{ .tag = .identifier, .start = 71 });
try list.append(ally, .{ .tag = .l_paren, .start = 75 });
try list.append(ally, .{ .tag = .string_literal, .start = 76 });
try list.append(ally, .{ .tag = .comma, .start = 113 });
try list.append(ally, .{ .tag = .period, .start = 115 });
try list.append(ally, .{ .tag = .l_brace, .start = 116 });
try list.append(ally, .{ .tag = .r_brace, .start = 117 });
try list.append(ally, .{ .tag = .r_paren, .start = 118 });
try list.append(ally, .{ .tag = .semicolon, .start = 119 });
try list.append(ally, .{ .tag = .r_brace, .start = 121 });
try list.append(ally, .{ .tag = .eof, .start = 123 });
const tags = list.items(.tag);
try testing.expectEqual(tags[1], .identifier);
try testing.expectEqual(tags[2], .equal);
try testing.expectEqual(tags[3], .builtin);
try testing.expectEqual(tags[4], .l_paren);
try testing.expectEqual(tags[5], .string_literal);
try testing.expectEqual(tags[6], .r_paren);
try testing.expectEqual(tags[7], .semicolon);
try testing.expectEqual(tags[8], .keyword_pub);
try testing.expectEqual(tags[9], .keyword_fn);
try testing.expectEqual(tags[10], .identifier);
try testing.expectEqual(tags[11], .l_paren);
try testing.expectEqual(tags[12], .r_paren);
try testing.expectEqual(tags[13], .identifier);
try testing.expectEqual(tags[14], .bang);
try testing.expectEqual(tags[15], .identifier);
try testing.expectEqual(tags[16], .l_brace);
try testing.expectEqual(tags[17], .identifier);
try testing.expectEqual(tags[18], .period);
try testing.expectEqual(tags[19], .identifier);
try testing.expectEqual(tags[20], .period);
try testing.expectEqual(tags[21], .identifier);
try testing.expectEqual(tags[22], .l_paren);
try testing.expectEqual(tags[23], .string_literal);
try testing.expectEqual(tags[24], .comma);
try testing.expectEqual(tags[25], .period);
try testing.expectEqual(tags[26], .l_brace);
try testing.expectEqual(tags[27], .r_brace);
try testing.expectEqual(tags[28], .r_paren);
try testing.expectEqual(tags[29], .semicolon);
try testing.expectEqual(tags[30], .r_brace);
try testing.expectEqual(tags[31], .eof);
}
test "ensure capacity on empty list" {
const ally = testing.allocator;
const Foo = struct {
a: u32,
b: u8,
};
var list = MultiArrayList(Foo){};
defer list.deinit(ally);
try list.ensureTotalCapacity(ally, 2);
list.appendAssumeCapacity(.{ .a = 1, .b = 2 });
list.appendAssumeCapacity(.{ .a = 3, .b = 4 });
try testing.expectEqualSlices(u32, &[_]u32{ 1, 3 }, list.items(.a));
try testing.expectEqualSlices(u8, &[_]u8{ 2, 4 }, list.items(.b));
list.len = 0;
list.appendAssumeCapacity(.{ .a = 5, .b = 6 });
list.appendAssumeCapacity(.{ .a = 7, .b = 8 });
try testing.expectEqualSlices(u32, &[_]u32{ 5, 7 }, list.items(.a));
try testing.expectEqualSlices(u8, &[_]u8{ 6, 8 }, list.items(.b));
list.len = 0;
try list.ensureTotalCapacity(ally, 16);
list.appendAssumeCapacity(.{ .a = 9, .b = 10 });
list.appendAssumeCapacity(.{ .a = 11, .b = 12 });
try testing.expectEqualSlices(u32, &[_]u32{ 9, 11 }, list.items(.a));
try testing.expectEqualSlices(u8, &[_]u8{ 10, 12 }, list.items(.b));
}
test "insert elements" {
const ally = testing.allocator;
const Foo = struct {
a: u8,
b: u32,
};
var list = MultiArrayList(Foo){};
defer list.deinit(ally);
try list.insert(ally, 0, .{ .a = 1, .b = 2 });
try list.ensureUnusedCapacity(ally, 1);
list.insertAssumeCapacity(1, .{ .a = 2, .b = 3 });
try testing.expectEqualSlices(u8, &[_]u8{ 1, 2 }, list.items(.a));
try testing.expectEqualSlices(u32, &[_]u32{ 2, 3 }, list.items(.b));
}
test "union" {
const ally = testing.allocator;
const Foo = union(enum) {
a: u32,
b: []const u8,
};
var list = MultiArrayList(Foo){};
defer list.deinit(ally);
try testing.expectEqual(@as(usize, 0), list.items(.tags).len);
try list.ensureTotalCapacity(ally, 2);
list.appendAssumeCapacity(.{ .a = 1 });
list.appendAssumeCapacity(.{ .b = "zigzag" });
try testing.expectEqualSlices(meta.Tag(Foo), list.items(.tags), &.{ .a, .b });
try testing.expectEqual(@as(usize, 2), list.items(.tags).len);
list.appendAssumeCapacity(.{ .b = "foobar" });
try testing.expectEqualStrings("zigzag", list.items(.data)[1].b);
try testing.expectEqualStrings("foobar", list.items(.data)[2].b);
// Add 6 more things to force a capacity increase.
for (0..6) |i| {
try list.append(ally, .{ .a = @as(u32, @intCast(4 + i)) });
}
try testing.expectEqualSlices(
meta.Tag(Foo),
&.{ .a, .b, .b, .a, .a, .a, .a, .a, .a },
list.items(.tags),
);
try testing.expectEqual(Foo{ .a = 1 }, list.get(0));
try testing.expectEqual(Foo{ .b = "zigzag" }, list.get(1));
try testing.expectEqual(Foo{ .b = "foobar" }, list.get(2));
try testing.expectEqual(Foo{ .a = 4 }, list.get(3));
try testing.expectEqual(Foo{ .a = 5 }, list.get(4));
try testing.expectEqual(Foo{ .a = 6 }, list.get(5));
try testing.expectEqual(Foo{ .a = 7 }, list.get(6));
try testing.expectEqual(Foo{ .a = 8 }, list.get(7));
try testing.expectEqual(Foo{ .a = 9 }, list.get(8));
list.shrinkAndFree(ally, 3);
try testing.expectEqual(@as(usize, 3), list.items(.tags).len);
try testing.expectEqualSlices(meta.Tag(Foo), list.items(.tags), &.{ .a, .b, .b });
try testing.expectEqual(Foo{ .a = 1 }, list.get(0));
try testing.expectEqual(Foo{ .b = "zigzag" }, list.get(1));
try testing.expectEqual(Foo{ .b = "foobar" }, list.get(2));
}
test "sorting a span" {
var list: MultiArrayList(struct { score: u32, chr: u8 }) = .{};
defer list.deinit(testing.allocator);
try list.ensureTotalCapacity(testing.allocator, 42);
for (
// zig fmt: off
[42]u8{ 'b', 'a', 'c', 'a', 'b', 'c', 'b', 'c', 'b', 'a', 'b', 'a', 'b', 'c', 'b', 'a', 'a', 'c', 'c', 'a', 'c', 'b', 'a', 'c', 'a', 'b', 'b', 'c', 'c', 'b', 'a', 'b', 'a', 'b', 'c', 'b', 'a', 'a', 'c', 'c', 'a', 'c' },
[42]u32{ 1, 1, 1, 2, 2, 2, 3, 3, 4, 3, 5, 4, 6, 4, 7, 5, 6, 5, 6, 7, 7, 8, 8, 8, 9, 9, 10, 9, 10, 11, 10, 12, 11, 13, 11, 14, 12, 13, 12, 13, 14, 14 },
// zig fmt: on
) |chr, score| {
list.appendAssumeCapacity(.{ .chr = chr, .score = score });
}
const sliced = list.slice();
list.sortSpan(6, 21, struct {
chars: []const u8,
fn lessThan(ctx: @This(), a: usize, b: usize) bool {
return ctx.chars[a] < ctx.chars[b];
}
}{ .chars = sliced.items(.chr) });
var i: u32 = undefined;
var j: u32 = 6;
var c: u8 = 'a';
while (j < 21) {
i = j;
j += 5;
var n: u32 = 3;
for (sliced.items(.chr)[i..j], sliced.items(.score)[i..j]) |chr, score| {
try testing.expectEqual(score, n);
try testing.expectEqual(chr, c);
n += 1;
}
c += 1;
}
}
test "0 sized struct field" {
const ally = testing.allocator;
const Foo = struct {
a: u0,
b: f32,
};
var list = MultiArrayList(Foo){};
defer list.deinit(ally);
try testing.expectEqualSlices(u0, &[_]u0{}, list.items(.a));
try testing.expectEqualSlices(f32, &[_]f32{}, list.items(.b));
try list.append(ally, .{ .a = 0, .b = 42.0 });
try testing.expectEqualSlices(u0, &[_]u0{0}, list.items(.a));
try testing.expectEqualSlices(f32, &[_]f32{42.0}, list.items(.b));
try list.insert(ally, 0, .{ .a = 0, .b = -1.0 });
try testing.expectEqualSlices(u0, &[_]u0{ 0, 0 }, list.items(.a));
try testing.expectEqualSlices(f32, &[_]f32{ -1.0, 42.0 }, list.items(.b));
list.swapRemove(list.len - 1);
try testing.expectEqualSlices(u0, &[_]u0{0}, list.items(.a));
try testing.expectEqualSlices(f32, &[_]f32{-1.0}, list.items(.b));
}
test "0 sized struct" {
const ally = testing.allocator;
const Foo = struct {
a: u0,
};
var list = MultiArrayList(Foo){};
defer list.deinit(ally);
try testing.expectEqualSlices(u0, &[_]u0{}, list.items(.a));
try list.append(ally, .{ .a = 0 });
try testing.expectEqualSlices(u0, &[_]u0{0}, list.items(.a));
try list.insert(ally, 0, .{ .a = 0 });
try testing.expectEqualSlices(u0, &[_]u0{ 0, 0 }, list.items(.a));
list.swapRemove(list.len - 1);
try testing.expectEqualSlices(u0, &[_]u0{0}, list.items(.a));
}