Misc improvements for future binary snapshot API (#13525)

Extracted out the raw `src/terminal` changes needed for the future
snapshot work, 4 separate changes. These are uncontroversial and
relatively simple, summarized below. Tests AI assisted but the rest
including commit messages, this PR message, etc. all organic.

* **Add iterator to ref counted set.** Iterate over live entries and
their IDs. Const, doesn't mutate the set.
* **lib.Enum produces stable enums for Zig.** Basically the same as C
except it uses the smallest fitting integer including the holes.
* **PageList: a couple helpers for manually creating pages.** There is
`PageList.Builder` for creating a new pagelist and
`PageList.allocatePage` for modifying an existing one. This allows
PageList construction from raw pages.
This commit is contained in:
Mitchell Hashimoto
2026-07-30 11:21:45 -07:00
committed by GitHub
3 changed files with 741 additions and 14 deletions

View File

@@ -5,18 +5,16 @@ const Target = @import("target.zig").Target;
/// if we're targeting C, otherwise a Zig enum with smallest possible
/// backing type.
///
/// In all cases, the enum keys will be created in the order given.
/// For C ABI, this means that the order MUST NOT be changed in order
/// to preserve ABI compatibility. You can set a key to null to
/// remove it from the Zig enum while keeping the "hole" in the C enum
/// to preserve ABI compatibility.
/// In all cases, each enum value is its index in `keys`. The order MUST NOT
/// be changed when the integer values are part of an ABI or serialized format.
/// A null key removes that field while preserving its integer hole.
///
/// C detection is up to the caller, since there are multiple ways
/// to do that. We rely on the `target` parameter to determine whether we
/// should create a C compatible enum or a Zig enum.
///
/// For the Zig enum, the enum value is not guaranteed to be stable, so
/// it shouldn't be relied for things like serialization.
/// C enums use `c_int`. Zig enums use the smallest unsigned integer that can
/// represent every key index, including holes.
pub fn Enum(
target: Target,
keys: []const ?[:0]const u8,
@@ -24,14 +22,12 @@ pub fn Enum(
var names_raw: [keys.len][]const u8 = undefined;
var values_raw: [keys.len]comptime_int = undefined;
const names_actual, const values_actual = kv: {
// Remove any holes in the input (null values). As mentioned in the
// function docs, in the event of the C ABI, we need to preserve the
// original value, so we account for that.
// Remove null fields while preserving their positions as integer holes.
var to: comptime_int = 0;
for (0..keys.len) |from| {
if (keys[from]) |key| {
names_raw[to] = key;
values_raw[to] = if (target == .c) from else to;
values_raw[to] = from;
to += 1;
}
}
@@ -41,7 +37,7 @@ pub fn Enum(
const TagInt = switch (target) {
.c => c_int,
.zig => std.math.IntFittingRange(0, names_actual.len - 1),
.zig => std.math.IntFittingRange(0, keys.len - 1),
};
return @Enum(TagInt, .exhaustive, names_actual, &(values: {
@@ -69,7 +65,7 @@ test "c" {
try testing.expectEqual(c_int, info.tag_type);
}
test "abi by removing a key" {
test "stable values when removing a key" {
const testing = std.testing;
// C
{
@@ -84,10 +80,28 @@ test "abi by removing a key" {
const T = Enum(.zig, &.{ "a", "b", null, "d" });
const info = @typeInfo(T).@"enum";
try testing.expectEqual(u2, info.tag_type);
try testing.expectEqual(2, @intFromEnum(T.d));
try testing.expectEqual(3, @intFromEnum(T.d));
}
}
test "zig backing integer includes trailing holes" {
const testing = std.testing;
const T = Enum(.zig, &.{ "a", null, null, null, null });
const info = @typeInfo(T).@"enum";
try testing.expectEqual(u3, info.tag_type);
try testing.expectEqual(0, @intFromEnum(T.a));
}
test "zig values remain stable across multiple holes" {
const testing = std.testing;
const T = Enum(.zig, &.{ null, "b", null, "d", null, "f" });
const info = @typeInfo(T).@"enum";
try testing.expectEqual(u3, info.tag_type);
try testing.expectEqual(1, @intFromEnum(T.b));
try testing.expectEqual(3, @intFromEnum(T.d));
try testing.expectEqual(5, @intFromEnum(T.f));
}
/// Verify that for every key in enum T, there is a matching declaration in
/// `ghostty.h` with the correct value. This should only ever be called inside a `test`
/// because the `ghostty.h` module is only available then.

View File

@@ -3866,6 +3866,125 @@ pub fn increaseCapacity(
return new_node;
}
/// Allocate a new page using the PageList's memory pools.
///
/// The page is detached: it doesn't contribute to the memory limits or
/// row counts or anything in the PageList. The caller must call `finalize`
/// to add it to the PageList at the appropriate place, or `deinit` to
/// throw it away.
pub fn allocatePage(
self: *PageList,
capacity: Capacity,
) Allocator.Error!PageAllocation {
return .{
.destination = self,
.node = try createPageExt(
&self.pool,
.{ .cap = capacity },
&self.page_serial,
null,
),
};
}
/// One PageList-pooled page which has not yet joined the live page sequence.
pub const PageAllocation = struct {
destination: *PageList,
node: ?*List.Node,
/// Return the fresh page storage for the caller to populate.
pub fn page(self: *PageAllocation) *Page {
return self.node.?.pageAssumeResident();
}
/// Release an uncommitted page back to its PageList's pools.
///
/// This is safe to call after `finalize` succeeds, so callers can defer
/// it unconditionally.
pub fn deinit(self: *PageAllocation) void {
const node = self.node orelse return;
destroyNodeExt(
&self.destination.pool,
node,
null,
);
self.node = null;
}
pub const Location = union(enum) {
/// Prepend the page to the start of the list (oldest history).
prepend,
};
/// Finalize this complete page and transfer its ownership to the PageList.
/// The parameter determines where it goes into the PageList.
///
/// Existing pages and tracked pins keep their identity. A pinned viewport
/// keeps showing the same content while its cached absolute row offset
/// moves down by the number of newly inserted rows.
pub fn finalize(self: *PageAllocation, location: Location) FinalizeError!void {
switch (location) {
.prepend => return try self.prepend(),
}
}
pub const FinalizeError = error{
InvalidPageDimensions,
RowCountOverflow,
PageSizeOverflow,
MaxSizeExceeded,
MaxLinesExceeded,
};
fn prepend(self: *PageAllocation) FinalizeError!void {
const destination = self.destination;
const node = self.node.?;
// Validate the populated page and all resulting accounting before
// publishing the detached node into the live list.
if (node.cols() == 0 or node.rows() == 0) return error.InvalidPageDimensions;
const total_rows = std.math.add(
usize,
destination.total_rows,
node.rows(),
) catch return error.RowCountOverflow;
const node_size: usize = switch (node.owned) {
.pool => PagePool.item_size,
.heap => node.pageAssumeResident().memory.len,
};
const page_size = std.math.add(
usize,
destination.page_size,
node_size,
) catch return error.PageSizeOverflow;
// Restored history is exact data, so reject a page which cannot
// coexist with the receiving PageList's configured limits.
if (page_size > destination.limits.max(.bytes)) {
return error.MaxSizeExceeded;
}
if (total_rows - destination.rows > destination.limits.max(.lines)) {
return error.MaxLinesExceeded;
}
// No fallible work remains. Publish the page and update every cached
// quantity affected by inserting rows above the existing first page.
errdefer comptime unreachable;
destination.pages.prepend(node);
destination.page_size = page_size;
destination.total_rows = total_rows;
if (destination.viewport == .pin) {
if (destination.viewport_pin_row_offset) |*offset| {
offset.* += node.rows();
}
}
destination.page_compression.markActivity();
destination.assertIntegrity();
self.node = null;
}
};
/// Options for createPage and createPageExt.
const CreatePage = struct {
/// The capacity to allocate the page with.
@@ -6829,6 +6948,513 @@ pub const Pin = struct {
}
};
/// Build up a PageList manually from a set of Pages.
///
/// This data structure is transactional: `deinit` releases every page
/// until `finish` is called. This keeps the ownership clear: a complete
/// PageList either owns all its pages or doesn't.
///
/// This was specifically built to help facilitate snapshot decoding
/// which transfers pages directly, but could be generally useful
/// for other purposes as well.
pub const Builder = struct {
pool: MemoryPool,
pages: List = .{},
page_serial: u64 = 0,
page_size: usize = 0,
options: Options,
finished: bool = false,
/// Initialize an empty builder. The options are the final state
/// of the PageList and some validation is done on the finish call
/// to ensure you built up a proper PageList according to those options.
pub fn init(
alloc: Allocator,
options: Options,
) Allocator.Error!Builder {
return .{
.pool = try MemoryPool.init(
alloc,
pageAllocator(),
page_preheat,
),
.options = options,
};
}
/// Release all pages when restoration does not finish.
///
/// This is safe to call after `finish` succeeds, so callers can defer it
/// unconditionally.
pub fn deinit(self: *Builder) void {
if (self.finished) return;
// Free all our in-progress pages
while (self.pages.popFirst()) |node| destroyNodeExt(
&self.pool,
node,
&self.page_size,
);
// Free memory pool
self.pool.deinit();
self.* = undefined;
}
/// Allocate a new page into the PageList with the given capacity.
///
/// The caller can then take this page and populate it. When `finish`
/// is called, ownership is transferred to the resulting PageList.
/// Until then, this Builder owns the page.
pub fn allocatePage(
self: *Builder,
capacity: Capacity,
) Allocator.Error!*Page {
const node = try createPageExt(
&self.pool,
.{ .cap = capacity },
&self.page_serial,
&self.page_size,
);
self.pages.append(node);
return node.pageAssumeResident();
}
pub const FinishError = Allocator.Error || error{
InvalidDimensions,
InvalidPageDimensions,
NoPages,
InsufficientRows,
};
/// Validate the decoded pages and transfer them into a live PageList.
/// After this succeeds, `deinit` is a no-op because all resources have
/// transferred to the PageList.
pub fn finish(self: *Builder) FinishError!PageList {
// These are basic validations but they're cheap to do and
// we want to be careful we don't let corruption from untrusted
// sources into our PageList which asserts this.
if (self.options.cols == 0 or self.options.rows == 0) {
return error.InvalidDimensions;
}
if (self.pages.first == null) return error.NoPages;
// Manually count our total rows at this point which we'll
// need for our PageList cache as well as a safety check.
const total_rows: usize = total_rows: {
var total_rows: usize = 0;
var node = self.pages.first;
while (node) |current| : (node = current.next) {
if (current.cols() == 0 or current.rows() == 0) {
return error.InvalidPageDimensions;
}
total_rows += current.rows();
}
if (total_rows < self.options.rows) return error.InsufficientRows;
break :total_rows total_rows;
};
// Get our active pin
const active_top: Pin = active_top: {
var rem = self.options.rows;
var node = self.pages.last;
while (node) |current| : (node = current.prev) {
if (rem <= current.rows()) break :active_top .{
.node = current,
.y = current.rows() - rem,
};
rem -= current.rows();
} else unreachable;
};
// Set our viewport up to the active
const viewport_pin = try self.pool.pins.create();
errdefer self.pool.pins.destroy(viewport_pin);
viewport_pin.* = active_top;
// Setup our one viewport tracked pin
var tracked_pins: PinSet = .{};
errdefer tracked_pins.deinit(self.pool.alloc);
try tracked_pins.putNoClobber(self.pool.alloc, viewport_pin, {});
// Initialize limits
var limits: Limits = .init(self.options.cols, self.options.rows);
limits.set(.bytes, self.options.max_size);
limits.set(.lines, self.options.max_lines);
const result: PageList = .{
.cols = self.options.cols,
.rows = self.options.rows,
.pool = self.pool,
.pages = self.pages,
.page_serial = self.page_serial,
.page_serial_epoch = 0,
.page_size = self.page_size,
.limits = limits,
.total_rows = total_rows,
.tracked_pins = tracked_pins,
.viewport = .{ .active = {} },
.viewport_pin = viewport_pin,
.viewport_pin_row_offset = null,
};
result.assertIntegrity();
self.finished = true;
return result;
}
};
test "PageList Builder transfers mixed-width pages" {
const testing = std.testing;
// Build two populated pages whose widths differ from each other and from
// the final active-area width. The first page also includes one row of
// incidental history above the three-row active area.
var result: PageList = result: {
var builder = try Builder.init(testing.allocator, .{
.cols = 4,
.rows = 3,
.max_size = null,
.max_lines = null,
});
defer builder.deinit();
const first = try builder.allocatePage(.{
.cols = 2,
.rows = 2,
});
first.size.rows = 2;
first.getRowAndCell(0, 0).cell.* = .init('A');
const second = try builder.allocatePage(.{
.cols = 4,
.rows = 2,
});
second.size.rows = 2;
second.getRowAndCell(0, 0).cell.* = .init('B');
break :result try builder.finish();
};
defer result.deinit();
// Successful finish transfers ownership and initializes the PageList's
// desired geometry, viewport, and required tracked viewport pin.
try testing.expectEqual(@as(size.CellCountInt, 4), result.cols);
try testing.expectEqual(@as(size.CellCountInt, 3), result.rows);
try testing.expectEqual(@as(usize, 2), result.totalPages());
try testing.expectEqual(@as(usize, 1), result.countTrackedPins());
try testing.expectEqual(Viewport.active, result.viewport);
// Complete pages and their contents are preserved in insertion order,
// including widths which have not yet been reflowed.
const screen_top = result.getTopLeft(.screen);
try testing.expectEqual(@as(size.CellCountInt, 2), screen_top.node.cols());
try testing.expectEqual(@as(u21, 'A'), screen_top
.node.page().getRowAndCell(0, 0).cell.codepoint());
// The active area is calculated backward from the newest page, so it
// begins at row one of the oldest page and leaves row zero as history.
const active_top = result.getTopLeft(.active);
try testing.expectEqual(screen_top.node, active_top.node);
try testing.expectEqual(@as(size.CellCountInt, 1), active_top.y);
try testing.expectEqual(@as(size.CellCountInt, 4), active_top
.node.next.?.cols());
try testing.expectEqual(@as(u21, 'B'), active_top
.node.next.?.page().getRowAndCell(0, 0).cell.codepoint());
result.assertIntegrity();
}
test "PageList Builder validates the finished list" {
const testing = std.testing;
// The desired PageList geometry must describe a non-empty screen.
{
var builder = try Builder.init(testing.allocator, .{
.cols = 0,
.rows = 1,
});
defer builder.deinit();
try testing.expectError(error.InvalidDimensions, builder.finish());
}
// A PageList cannot be finished without any backing pages.
{
var builder = try Builder.init(testing.allocator, .{
.cols = 1,
.rows = 1,
});
defer builder.deinit();
try testing.expectError(error.NoPages, builder.finish());
}
// Allocated capacity alone is insufficient: callers must populate a
// nonzero logical page size before transferring ownership.
{
var builder = try Builder.init(testing.allocator, .{
.cols = 1,
.rows = 1,
});
defer builder.deinit();
const page = try builder.allocatePage(.{ .cols = 1, .rows = 1 });
page.size.rows = 0;
try testing.expectError(
error.InvalidPageDimensions,
builder.finish(),
);
}
// The populated pages must contain enough rows to cover the active area.
{
var builder = try Builder.init(testing.allocator, .{
.cols = 1,
.rows = 2,
});
defer builder.deinit();
const page = try builder.allocatePage(.{ .cols = 1, .rows = 1 });
page.size.rows = 1;
try testing.expectError(error.InsufficientRows, builder.finish());
}
}
test "PageList Builder finish is transactional on allocation failure" {
const testing = std.testing;
// Construct a valid builder so finish reaches its fallible bookkeeping
// allocations after all page and geometry validation succeeds.
var failing = testing.FailingAllocator.init(testing.allocator, .{});
var builder = try Builder.init(failing.allocator(), .{
.cols = 1,
.rows = 1,
});
defer builder.deinit();
const page = try builder.allocatePage(.{ .cols = 1, .rows = 1 });
page.size.rows = 1;
// The pools are preheated, so the next general allocation is the tracked
// viewport pin map created by finish.
failing.fail_index = failing.alloc_index;
try testing.expectError(error.OutOfMemory, builder.finish());
try testing.expect(failing.has_induced_failure);
// Failed finish leaves page ownership with the builder so its normal
// deinit path can release the still-linked page.
try testing.expect(builder.pages.first != null);
try testing.expectEqual(builder.pages.first, builder.pages.last);
}
test "PageList PageAllocation finalizes pages and preserves live state" {
const testing = std.testing;
// Build an existing list with history, a two-row active area, an external
// active pin, and a pinned viewport whose absolute offset is cached.
var result: PageList = result: {
var builder = try Builder.init(testing.allocator, .{
.cols = 4,
.rows = 2,
.max_size = null,
.max_lines = null,
});
defer builder.deinit();
const first = try builder.allocatePage(.{ .cols = 3, .rows = 2 });
first.size.rows = 2;
first.getRowAndCell(0, 0).cell.* = .init('C');
const second = try builder.allocatePage(.{ .cols = 4, .rows = 2 });
second.size.rows = 2;
second.getRowAndCell(0, 0).cell.* = .init('D');
break :result try builder.finish();
};
defer result.deinit();
const old_first = result.pages.first.?;
const old_last = result.pages.last.?;
const active_top = result.getTopLeft(.active);
const tracked_active = try result.trackPin(active_top);
result.scroll(.{ .row = 1 });
try testing.expectEqual(Viewport.pin, result.viewport);
try testing.expectEqual(@as(usize, 1), result.scrollbar().offset);
// Prepend differently sized historical pages newest-first. Each page is
// populated while detached and joins the live list only on success.
{
var allocation = try result.allocatePage(.{ .cols = 4, .rows = 1 });
defer allocation.deinit();
const page = allocation.page();
page.size.rows = 1;
page.getRowAndCell(0, 0).cell.* = .init('B');
try allocation.finalize(.prepend);
}
{
var allocation = try result.allocatePage(.{ .cols = 2, .rows = 2 });
defer allocation.deinit();
const page = allocation.page();
page.size.rows = 2;
page.getRowAndCell(0, 0).cell.* = .init('A');
try allocation.finalize(.prepend);
}
// Repeated prepends reconstruct oldest-to-newest order without replacing
// any existing nodes or tracked pins.
try testing.expectEqual(@as(usize, 4), result.totalPages());
try testing.expectEqual(@as(usize, 7), result.total_rows);
try testing.expectEqual(old_last, result.pages.last.?);
try testing.expectEqual(old_first, result.pages.first.?.next.?.next.?);
try testing.expectEqual(
@as(u21, 'A'),
result.pages.first.?.page().getRowAndCell(0, 0).cell.codepoint(),
);
try testing.expectEqual(
@as(u21, 'B'),
result.pages.first.?.next.?
.page().getRowAndCell(0, 0).cell.codepoint(),
);
try testing.expect(active_top.eql(result.getTopLeft(.active)));
try testing.expect(active_top.eql(tracked_active.*));
// The viewport remains pinned to the same content, while its cached row
// offset and the scrollbar total include the three new historical rows.
try testing.expectEqual(Viewport.pin, result.viewport);
try testing.expectEqual(old_first, result.viewport_pin.node);
try testing.expectEqual(@as(size.CellCountInt, 1), result.viewport_pin.y);
const scrollbar_state = result.scrollbar();
try testing.expectEqual(@as(usize, 7), scrollbar_state.total);
try testing.expectEqual(@as(usize, 4), scrollbar_state.offset);
try testing.expectEqual(@as(usize, 2), scrollbar_state.len);
result.assertIntegrity();
}
test "PageList PageAllocation stays detached until finalize" {
const testing = std.testing;
var result = try init(testing.allocator, .{
.cols = 1,
.rows = 1,
.max_size = null,
.max_lines = null,
});
defer result.deinit();
const initial_first = result.pages.first.?;
const initial_last = result.pages.last.?;
const initial_total_rows = result.total_rows;
const initial_page_size = result.page_size;
// Allocating and populating a detached page does not alter any live list
// links or accounting. Deinit returns it to the same PageList pools.
var detached = try result.allocatePage(.{ .cols = 1, .rows = 1 });
detached.page().size.rows = 1;
try testing.expectEqual(initial_first, result.pages.first.?);
try testing.expectEqual(initial_last, result.pages.last.?);
try testing.expectEqual(initial_total_rows, result.total_rows);
try testing.expectEqual(initial_page_size, result.page_size);
result.assertIntegrity();
detached.deinit();
result.assertIntegrity();
// Invalid populated dimensions leave ownership with the allocation so it
// can still be released normally.
var invalid = try result.allocatePage(.{ .cols = 1, .rows = 1 });
defer invalid.deinit();
try testing.expectError(
error.InvalidPageDimensions,
invalid.finalize(.prepend),
);
try testing.expectEqual(initial_first, result.pages.first.?);
try testing.expectEqual(initial_last, result.pages.last.?);
try testing.expectEqual(initial_total_rows, result.total_rows);
try testing.expectEqual(initial_page_size, result.page_size);
result.assertIntegrity();
}
test "PageList PageAllocation rejects limits before modifying the destination" {
const testing = std.testing;
var result = try init(testing.allocator, .{
.cols = 1,
.rows = 1,
.max_size = 0,
.max_lines = null,
});
defer result.deinit();
// The effective minimum permits one complete page beyond the active page.
// Fill that allowance so the following allocation exceeds the byte limit.
{
var allocation = try result.allocatePage(.{ .cols = 1, .rows = 1 });
defer allocation.deinit();
allocation.page().size.rows = 1;
try allocation.finalize(.prepend);
}
const before_first = result.pages.first.?;
const before_total_rows = result.total_rows;
const before_page_size = result.page_size;
var allocation = try result.allocatePage(.{ .cols = 1, .rows = 1 });
defer allocation.deinit();
allocation.page().size.rows = 1;
try testing.expectError(
error.MaxSizeExceeded,
allocation.finalize(.prepend),
);
try testing.expectEqual(before_first, result.pages.first.?);
try testing.expectEqual(before_total_rows, result.total_rows);
try testing.expectEqual(before_page_size, result.page_size);
result.assertIntegrity();
}
test "PageList PageAllocation allocation failure leaves list unchanged" {
const testing = std.testing;
var failing = testing.FailingAllocator.init(testing.allocator, .{});
var result = try init(failing.allocator(), .{
.cols = 1,
.rows = 1,
.max_size = null,
.max_lines = null,
});
defer result.deinit();
const initial_first = result.pages.first.?;
const initial_total_rows = result.total_rows;
const initial_page_size = result.page_size;
// Existing pool capacity is deliberately an implementation detail. Allow
// preheated slots to succeed until allocation reaches node-pool growth.
failing.fail_index = failing.alloc_index;
var allocations: [64]PageAllocation = undefined;
var allocation_count: usize = 0;
defer for (allocations[0..allocation_count]) |*allocation| {
allocation.deinit();
};
var failed = false;
for (0..64) |_| {
allocations[allocation_count] = result.allocatePage(.{
.cols = 1,
.rows = 1,
}) catch |err| {
try testing.expectEqual(error.OutOfMemory, err);
failed = true;
break;
};
allocation_count += 1;
}
try testing.expect(failed);
try testing.expect(failing.has_induced_failure);
// Detached allocations and failed pool growth never publish into the live
// list; the deferred cleanup returns every successful allocation.
try testing.expectEqual(initial_first, result.pages.first.?);
try testing.expectEqual(initial_total_rows, result.total_rows);
try testing.expectEqual(initial_page_size, result.page_size);
result.assertIntegrity();
}
fn mixedWidthPinListForTest(alloc: Allocator) !PageList {
var result = try init(alloc, .{ .cols = 2, .rows = 1 });
errdefer result.deinit();

View File

@@ -1,5 +1,6 @@
const std = @import("std");
const assert = @import("../quirks.zig").inlineAssert;
const testing = std.testing;
const size = @import("size.zig");
const Offset = size.Offset;
@@ -447,6 +448,47 @@ pub fn RefCountedSet(
return self.living;
}
/// A live entry returned by `Iterator`.
pub const Entry = struct {
id: Id,
value_ptr: *T,
};
/// Iterates live entries in ascending ID order.
///
/// Released entries whose reference count reached zero are skipped.
/// Any mutation of the set invalidates the iterator.
pub const Iterator = struct {
items: [*]Item,
id: Id = 1,
end: Id,
pub fn next(self: *Iterator) ?Entry {
while (self.id < self.end) {
const id = self.id;
self.id += 1;
const item = &self.items[id];
if (item.meta.ref == 0) continue;
return .{
.id = id,
.value_ptr = &item.value,
};
}
return null;
}
};
/// Return an iterator over the live entries in this set.
pub fn iterator(self: *const Self, base: anytype) Iterator {
return .{
.items = self.items.ptr(base),
.end = self.next_id,
};
}
/// Delete an item, removing any references from
/// the table, and freeing its ID to be reused.
fn deleteItem(self: *Self, base: anytype, id: Id, ctx: Context) void {
@@ -721,3 +763,48 @@ pub fn RefCountedSet(
}
};
}
test "iterator visits live entries in ID order" {
const alloc = testing.allocator;
const TestSet = RefCountedSet(
u32,
u16,
u16,
struct {
pub fn hash(_: *const @This(), value: u32) u64 {
return std.hash.int(value);
}
pub fn eql(_: *const @This(), a: u32, b: u32) bool {
return a == b;
}
},
);
const layout: TestSet.Layout = .init(8);
const buf = try alloc.alignedAlloc(
u8,
TestSet.base_align,
layout.total_size,
);
defer alloc.free(buf);
var set: TestSet = .init(.init(buf), layout, .{});
const first = try set.add(buf, 11);
const released = try set.add(buf, 22);
const last = try set.add(buf, 33);
_ = try set.add(buf, 11);
set.release(buf, released);
var it = set.iterator(buf);
const first_entry = it.next().?;
try testing.expectEqual(first, first_entry.id);
try testing.expectEqual(@as(u32, 11), first_entry.value_ptr.*);
const last_entry = it.next().?;
try testing.expectEqual(last, last_entry.id);
try testing.expectEqual(@as(u32, 33), last_entry.value_ptr.*);
try testing.expect(it.next() == null);
}