299 lines
8.9 KiB
Zig
299 lines
8.9 KiB
Zig
const std = @import("std");
|
|
const tracy = @import("tracy");
|
|
const builtin = @import("builtin");
|
|
const assert = std.debug.assert;
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
/// This array provides:
|
|
/// - O(1) insertion
|
|
/// - O(1) removal
|
|
/// - O(1) lookup
|
|
/// - Pointers and IDs stay stable when inserting and removing.
|
|
/// - Unique IDs for every inserted item (assuming that generation doesn't overflow)
|
|
///
|
|
/// For more details about this data structure checkout this podcast episode from Wookash:
|
|
/// * https://www.youtube.com/watch?v=ShSGHb65f3M
|
|
pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
|
|
assert(@typeInfo(Generation) == .int);
|
|
assert(@typeInfo(Index) == .int);
|
|
|
|
return struct {
|
|
const Self = @This();
|
|
|
|
// TODO: This struct will probably have paddings bytes inserted.
|
|
// It's not ideal for a data structure like this which will be commonly used.
|
|
//
|
|
// The `used` field could be packed into a bitset.
|
|
// But that would introduce a second `std.ArrayList` which would make managing the memory a PITA.
|
|
//
|
|
// So in the end idk if it's worth it.
|
|
pub const Slot = struct {
|
|
value: Value,
|
|
generation: Generation,
|
|
next_hole: ?Index,
|
|
used: bool
|
|
};
|
|
|
|
slots: std.ArrayList(Slot),
|
|
first_hole: ?Index,
|
|
last_hole: ?Index,
|
|
hole_count: usize,
|
|
|
|
const empty = Self{
|
|
.slots = .empty,
|
|
.first_hole = null,
|
|
.last_hole = null,
|
|
.hole_count = 0,
|
|
};
|
|
|
|
pub const Id = packed struct {
|
|
generation: Generation,
|
|
index: Index,
|
|
|
|
pub fn format(self: Id, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
|
try writer.print("Id{{ {}, {} }}", .{ self.index, self.generation });
|
|
}
|
|
};
|
|
|
|
pub const Iterator = struct {
|
|
slot_map: *Self,
|
|
index: Index,
|
|
|
|
pub fn next(self: *Iterator) ?Id {
|
|
while (self.index < self.slot_map.slots.items.len) {
|
|
const index = self.index;
|
|
const slot = self.slot_map.slots.items[index];
|
|
self.index += 1;
|
|
|
|
if (slot.used) {
|
|
return Id{
|
|
.index = @intCast(index),
|
|
.generation = slot.generation
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
};
|
|
|
|
pub fn clearRetainingCapacity(self: *Self) void {
|
|
const slots = self.slots;
|
|
slots.clearRetainingCapacity();
|
|
|
|
self.* = .init(slots);
|
|
}
|
|
|
|
fn insertHole(self: *Self, index: Index) void {
|
|
if (self.last_hole) |last_hole| {
|
|
self.slots.items[index].next_hole = last_hole;
|
|
self.last_hole = index;
|
|
} else {
|
|
self.first_hole = index;
|
|
self.last_hole = index;
|
|
}
|
|
self.hole_count += 1;
|
|
}
|
|
|
|
fn removeUnused(self: *Self) ?Index {
|
|
if (self.first_hole) |first_hole| {
|
|
self.first_hole = self.slots.items[first_hole].next_hole;
|
|
if (self.first_hole == null) {
|
|
self.last_hole = null;
|
|
}
|
|
self.hole_count -= 1;
|
|
return first_hole;
|
|
}
|
|
|
|
const capacity = @min(self.slots.capacity, std.math.maxInt(Index));
|
|
if (self.slots.items.len < capacity) {
|
|
const index: Index = @intCast(self.slots.items.len);
|
|
self.slots.items.len += 1;
|
|
self.slots.items[index] = Slot{
|
|
.value = undefined,
|
|
.generation = 0,
|
|
.next_hole = null,
|
|
.used = false
|
|
};
|
|
return index;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub fn ensureUnusedCapacity(self: *Self, gpa: Allocator, additional_count: usize) Allocator.Error!void {
|
|
if (additional_count > self.hole_count) {
|
|
const new_capacity, const overflow = @addWithOverflow(
|
|
self.slots.capacity,
|
|
additional_count - self.hole_count
|
|
);
|
|
if (overflow != 0) return error.OutOfMemory;
|
|
if (new_capacity >= std.math.maxInt(Index)) return error.OutOfMemory;
|
|
|
|
try self.slots.ensureTotalCapacity(gpa, new_capacity);
|
|
}
|
|
}
|
|
|
|
pub fn unusedCapacity(self: *Self) usize {
|
|
const capacity = @min(self.slots.capacity, std.math.maxInt(Index));
|
|
return capacity - self.slots.items.len + self.hole_count;
|
|
}
|
|
|
|
pub fn insertAssumeCapacity(self: *Self) Id {
|
|
const index = self.removeUnused().?;
|
|
const slot = &self.slots.items[index];
|
|
assert(!slot.used);
|
|
slot.used = true;
|
|
|
|
return Id{
|
|
.index = @intCast(index),
|
|
.generation = slot.generation
|
|
};
|
|
}
|
|
|
|
pub fn insert(self: *Self, gpa: Allocator) Allocator.Error!Id {
|
|
try self.ensureUnusedCapacity(gpa, 1);
|
|
return self.insertAssumeCapacity();
|
|
}
|
|
|
|
pub fn insertBounded(self: *Self) Allocator.Error!Id {
|
|
if (self.unusedCapacity() == 0) {
|
|
return error.OutOfMemory;
|
|
}
|
|
return self.insertAssumeCapacity();
|
|
}
|
|
|
|
pub fn exists(self: *Self, id: Id) bool {
|
|
if (id.index >= self.slots.items.len) {
|
|
return false;
|
|
}
|
|
|
|
const slot = self.slots.items[id.index];
|
|
|
|
return slot.used and slot.generation == id.generation;
|
|
}
|
|
|
|
pub fn getAssumeExists(self: *Self, id: Id) *Value {
|
|
assert(self.exists(id));
|
|
return &self.slots.items[id.index].value;
|
|
}
|
|
|
|
pub fn removeAssumeExists(self: *Self, id: Id) void {
|
|
assert(self.exists(id));
|
|
|
|
const slot = &self.slots.items[id.index];
|
|
|
|
slot.used = false;
|
|
slot.generation +%= 1;
|
|
self.insertHole(id.index);
|
|
}
|
|
|
|
pub fn remove(self: *Self, id: Id) bool {
|
|
if (!self.exists(id)) {
|
|
return false;
|
|
}
|
|
|
|
self.removeAssumeExists(id);
|
|
return true;
|
|
}
|
|
|
|
pub fn iterator(self: *Self) Iterator {
|
|
return Iterator{
|
|
.slot_map = self,
|
|
.index = 0
|
|
};
|
|
}
|
|
|
|
pub fn init(slots: std.ArrayList(Slot)) Self {
|
|
var self: Self = .empty;
|
|
self.slots = slots;
|
|
return self;
|
|
}
|
|
|
|
pub fn deinit(self: *Self, gpa: Allocator) void {
|
|
self.slots.deinit(gpa);
|
|
}
|
|
};
|
|
}
|
|
|
|
const TestMap = SlotMapType(u24, u8, void);
|
|
|
|
// TODO: Add more rigorous test for check if generation usage is nicely distributed.
|
|
|
|
test "insert & remove" {
|
|
const expect = std.testing.expect;
|
|
const gpa = std.testing.allocator;
|
|
|
|
var map: TestMap = .empty;
|
|
defer map.deinit(gpa);
|
|
|
|
const id1 = try map.insert(gpa);
|
|
try expect(map.exists(id1));
|
|
try expect(map.remove(id1));
|
|
try expect(!map.exists(id1));
|
|
try expect(!map.remove(id1));
|
|
|
|
const id2 = try map.insert(gpa);
|
|
try expect(map.exists(id2));
|
|
try expect(!map.exists(id1));
|
|
}
|
|
|
|
test "generation wrap around" {
|
|
const expectEqual = std.testing.expectEqual;
|
|
const gpa = std.testing.allocator;
|
|
|
|
var map: TestMap = .empty;
|
|
defer map.deinit(gpa);
|
|
|
|
// Grow array list so that at least 1 slot exists
|
|
const id1 = try map.insert(gpa);
|
|
map.removeAssumeExists(id1);
|
|
|
|
// Artificially increase generation count
|
|
map.slots.items[id1.index].generation = std.math.maxInt(@FieldType(TestMap.Id, "generation"));
|
|
|
|
// Check if generation wraps around
|
|
const id2 = try map.insert(gpa);
|
|
map.removeAssumeExists(id2);
|
|
try expectEqual(id1.index, id2.index);
|
|
try expectEqual(0, map.slots.items[id1.index].generation);
|
|
}
|
|
|
|
test "iterator" {
|
|
const expectEqual = std.testing.expectEqual;
|
|
const gpa = std.testing.allocator;
|
|
|
|
var map: TestMap = .empty;
|
|
defer map.deinit(gpa);
|
|
|
|
// Create array which has a hole
|
|
const id1 = try map.insert(gpa);
|
|
const id2 = try map.insert(gpa);
|
|
const id3 = try map.insert(gpa);
|
|
|
|
map.removeAssumeExists(id2);
|
|
|
|
var iter = map.iterator();
|
|
try expectEqual(id1, iter.next().?);
|
|
try expectEqual(id3, iter.next().?);
|
|
try expectEqual(null, iter.next());
|
|
}
|
|
|
|
test "clear retaining capacity" {
|
|
const expect = std.testing.expect;
|
|
const expectEqual = std.testing.expectEqual;
|
|
const gpa = std.testing.allocator;
|
|
|
|
var map: TestMap = .empty;
|
|
defer map.deinit(gpa);
|
|
|
|
const id1 = try map.insert(gpa);
|
|
try expect(map.exists(id1));
|
|
map.clearRetainingCapacity();
|
|
|
|
const id2 = try map.insert(gpa);
|
|
try expect(map.exists(id2));
|
|
|
|
try expectEqual(id1, id2);
|
|
}
|