forked from vv/efemra
1
0
Fork 0
efemra/src/containers/table.zig

420 lines
15 KiB
Zig

const builtin = @import("builtin");
const std = @import("std");
const assert = std.debug.assert;
const Queue = @import("queue.zig").Queue;
/// A generational collection. Items can be quickly looked up by key,
/// and quickly removed. The free indices are stored in a queue and refilled as new
/// items are added. Ids will remain valid for the lifetime of the object, and when
/// the object is destroyed, will no longer be found in the collection.
///
/// Table is stored as a Structure of Arrays, to improve cache locality.
///
/// Note:
/// Currently hashing of the key is done automatically (using the logic of AutoHashMap).
/// In the future maybe support can be added to pass in a custom hash strategy.
pub fn Table(comptime K: type, comptime V: type) type {
return struct {
const Self = @This();
/// The ID for the table (index + generation)
pub const Id = struct {
index: usize,
gen: u8,
};
// Collections sharing an index:
/// generations of the items in the table. incremented when an item is added to a previously-used slot.
gens: std.ArrayList(u8),
/// The actual values.
values: std.ArrayList(?V),
/// Keys for the table entries.
keys: std.ArrayList(?K),
// Other fields
/// List of indices which have previously been freed and are available to fill.
free_list: Queue(usize),
/// Used for mapping the key to its index.
lookup: std.AutoHashMap(K, usize),
/// The amount of items in the table (not the allocated size)
len: usize,
pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.gens = std.ArrayList(u8).init(allocator),
.values = std.ArrayList(?V).init(allocator),
.keys = std.ArrayList(?K).init(allocator),
.free_list = Queue(usize).init(allocator),
.lookup = std.AutoHashMap(K, usize).init(allocator),
.len = 0,
};
}
pub fn deinit(self: *Self) void {
self.len = 0;
self.gens.deinit();
self.values.deinit();
self.keys.deinit();
self.free_list.deinit();
self.lookup.deinit();
}
pub fn clear(self: *Self) void {
self.len = 0;
self.gens.clearRetainingCapacity();
self.values.clearRetainingCapacity();
self.keys.clearRetainingCapacity();
self.free_list.clearRetainingCapacity();
self.lookup.clearRetainingCapacity();
}
pub fn size(self: *const Self) usize {
return self.len;
}
pub fn exists(self: *const Self, id: Id) bool {
return id.index < self.values.items.len and id.gen == self.gens.items[id.index];
}
pub const AddResult = struct {
id: Id,
added: bool,
};
pub fn add(self: *Self, key: K, val: V) !AddResult {
if (self.find(key)) |id| {
return AddResult{
.id = id,
.added = false,
};
}
if (self.free_list.popOrNull()) |index| {
const gen = self.gens.items[index];
self.keys.items[index] = key;
self.values.items[index] = val;
try self.lookup.putNoClobber(key, index);
self.len += 1;
return AddResult{
.id = .{
.index = index,
.gen = gen,
},
.added = true,
};
} else {
self.len += 1;
try self.keys.append(key);
try self.values.append(val);
try self.gens.append(0);
assert(self.len == self.keys.items.len);
assert(self.keys.items.len == self.values.items.len);
assert(self.values.items.len == self.gens.items.len);
const index = self.keys.items.len - 1;
try self.lookup.putNoClobber(key, index);
return AddResult{
.id = .{
.index = index,
.gen = 0,
},
.added = true,
};
}
}
pub fn remove(self: *Self, id: Id) !V {
assert(self.len > 0);
const index = id.index;
const key = self.keys.items[index] orelse unreachable;
const removed = self.lookup.remove(key);
assert(removed);
self.keys.items[index] = null;
self.gens.items[index] += 1;
const val = self.values.items[index] orelse unreachable;
self.values.items[index] = null;
try self.free_list.push(index);
self.len -= 1;
return val;
}
pub fn get(self: *Self, id: Id) ?*V {
return if (self.exists(id)) &(self.values.items[id.index] orelse unreachable) else null;
}
pub fn find(self: *Self, key: K) ?Id {
if (self.lookup.get(key)) |index| {
const gen = self.gens.items[index];
return Id{
.index = index,
.gen = gen,
};
} else {
return null;
}
}
pub fn getKey(self: *Self, id: Id) ?K {
return if (self.exists(id)) (self.keys.items[id.index] orelse unreachable) else null;
}
};
}
const TestVal = struct {
a: u32,
b: u32,
};
test "general table test" {
var table = Table(u32, TestVal).init(std.testing.allocator);
defer table.deinit();
const first_result = try table.add(56, .{ .a = 42, .b = 87 });
try std.testing.expect(first_result.added);
try std.testing.expectEqual(@as(u8, 0), first_result.id.gen);
try std.testing.expectEqual(@as(usize, 0), first_result.id.index);
const second_result = try table.add(62, .{ .a = 1, .b = 12 });
try std.testing.expect(second_result.added);
try std.testing.expectEqual(@as(u8, 0), second_result.id.gen);
try std.testing.expectEqual(@as(usize, 1), second_result.id.index);
var second_id = table.find(62) orelse unreachable;
var second_val = table.get(second_id) orelse unreachable;
try std.testing.expectEqual(@as(u32, 1), second_val.a);
try std.testing.expectEqual(@as(u32, 12), second_val.b);
try std.testing.expectEqual(@as(usize, 2), table.size());
_ = try table.remove(first_result.id);
try std.testing.expect(!table.exists(first_result.id));
try std.testing.expectEqual(@as(usize, 1), table.size());
// Ensure the id does not invalidate after removal of another
second_val = table.get(second_id) orelse unreachable;
try std.testing.expectEqual(@as(u32, 1), second_val.a);
try std.testing.expectEqual(@as(u32, 12), second_val.b);
// Grab the id again and ensure that works too.
second_id = table.find(62) orelse unreachable;
second_val = table.get(second_id) orelse unreachable;
try std.testing.expectEqual(@as(u32, 1), second_val.a);
try std.testing.expectEqual(@as(u32, 12), second_val.b);
}
test "table across generation" {
var table = Table(u32, TestVal).init(std.testing.allocator);
defer table.deinit();
const first_result = try table.add(48, .{ .a = 1, .b = 2 });
_ = try table.add(28, .{ .a = 2, .b = 3 });
// remove first item, then add a new one, then try to access first item!
_ = try table.remove(table.find(48) orelse unreachable);
const second_result = try table.add(99, .{ .a = 2, .b = 3 });
try std.testing.expectEqual(@as(u8, 1), second_result.id.gen);
try std.testing.expect(!table.exists(first_result.id));
try std.testing.expectEqual(@as(?*TestVal, null), table.get(first_result.id));
}
/// Like Table, but values are refcounted. When duplicate items are added or retained, their
/// reference count is incremented. When items are released, their reference count is
/// decremented, and zero-count items are removed and returned. The ownership of the item then
/// transfers to the decrementer, for any memory cleanup that must happen.
pub fn RefTable(comptime K: type, comptime V: type) type {
return struct {
const Self = @This();
const InternalTable = Table(K, V);
pub const Id = InternalTable.Id;
/// Underlying table.
table: InternalTable,
/// Reference counts of the values.
ref_counts: std.ArrayList(usize),
pub fn init(allocator: std.mem.Allocator) Self {
return Self{
.table = Table(K, V).init(allocator),
.ref_counts = std.ArrayList(usize).init(allocator),
};
}
/// Expects that all references have been cleaned up and will assert if
/// dangling references remain. To ignore dangling references, call clear() first.
pub fn deinit(self: *Self) void {
self.table.deinit();
if (builtin.mode == .Debug or builtin.mode == .ReleaseSafe) {
for (self.ref_counts.items) |count| {
assert(count == 0);
}
}
self.ref_counts.deinit();
}
pub fn size(self: *const Self) usize {
return self.table.size();
}
pub fn remove(self: *Self, id: Id) !V {
self.ref_counts.items[id.index] = 0;
return self.table.remove(id);
}
/// Increment the reference count of the item.
pub fn retain(self: *Self, id: Id) bool {
if (!self.table.exists(id)) {
return false;
}
const ref_count = self.ref_counts.items[id.index];
self.ref_counts.items[id.index] = ref_count + 1;
return true;
}
/// Decrement the reference count of the item. If it reaches zero, the item will be removed
/// from the table and returned. It is the responsibility of the caller to free any
/// dynamic memory the value may point to.
pub fn release(self: *Self, id: Id) !?V {
if (!self.table.exists(id)) {
return null;
}
const index = id.index;
var ref_count = self.ref_counts.items[index];
ref_count -= 1;
self.ref_counts.items[index] = ref_count;
return if (ref_count == 0) try self.remove(id) else null;
}
/// Clear all items from the table. This should only be called if the items
/// do not point to dynamically allocated memory!
pub fn clear(self: *Self) void {
self.table.clear();
self.ref_counts.clearRetainingCapacity();
}
pub fn exists(self: *const Self, id: Id) bool {
return self.table.exists(id);
}
pub const AddResult = InternalTable.AddResult;
/// Add to the table. If the ID already exists, a new reference will be
/// added to the reference count.
pub fn add(self: *Self, key: K, val: V) !AddResult {
if (self.find(key)) |id| {
_ = self.retain(id);
return AddResult{
.id = id,
.added = false,
};
}
const result = try self.table.add(key, val);
assert(result.added);
if (result.id.index >= self.ref_counts.items.len) {
assert(result.id.index == self.ref_counts.items.len);
try self.ref_counts.append(1);
} else {
self.ref_counts.items[result.id.index] = 1;
}
return result;
}
pub fn get(self: *Self, id: Id) ?*V {
return self.table.get(id);
}
pub fn find(self: *Self, key: K) ?Id {
return self.table.find(key);
}
pub fn getKey(self: *Self, id: Id) ?K {
return self.table.getKey(id);
}
};
}
test "general ref table test" {
var table = RefTable(u32, TestVal).init(std.testing.allocator);
defer table.deinit();
const first_result = try table.add(56, .{ .a = 42, .b = 87 });
try std.testing.expect(first_result.added);
try std.testing.expectEqual(@as(u8, 0), first_result.id.gen);
try std.testing.expectEqual(@as(usize, 0), first_result.id.index);
const second_result = try table.add(62, .{ .a = 1, .b = 12 });
try std.testing.expect(second_result.added);
try std.testing.expectEqual(@as(u8, 0), second_result.id.gen);
try std.testing.expectEqual(@as(usize, 1), second_result.id.index);
var second_id = table.find(62) orelse unreachable;
var second_val = table.get(second_id) orelse unreachable;
try std.testing.expectEqual(@as(u32, 1), second_val.a);
try std.testing.expectEqual(@as(u32, 12), second_val.b);
try std.testing.expectEqual(@as(usize, 2), table.size());
_ = try table.remove(first_result.id);
try std.testing.expect(!table.exists(first_result.id));
try std.testing.expectEqual(@as(usize, 1), table.size());
// Ensure the id does not invalidate after removal of another
second_val = table.get(second_id) orelse unreachable;
try std.testing.expectEqual(@as(u32, 1), second_val.a);
try std.testing.expectEqual(@as(u32, 12), second_val.b);
// Grab the id again and ensure that works too.
second_id = table.find(62) orelse unreachable;
second_val = table.get(second_id) orelse unreachable;
try std.testing.expectEqual(@as(u32, 1), second_val.a);
try std.testing.expectEqual(@as(u32, 12), second_val.b);
table.clear();
}
test "ref counting" {
var table = RefTable(u32, TestVal).init(std.testing.allocator);
defer table.deinit();
var first_result = try table.add(12, .{ .a = 5, .b = 6 });
try std.testing.expectEqual(@as(usize, 1), table.size());
var val = (try table.release(first_result.id)) orelse unreachable;
try std.testing.expectEqual(@as(usize, 0), table.size());
try std.testing.expectEqual(@as(u32, 5), val.a);
try std.testing.expectEqual(@as(u32, 6), val.b);
first_result = try table.add(12, .{ .a = 6, .b = 5 });
try std.testing.expect(first_result.added);
try std.testing.expectEqual(@as(usize, 0), first_result.id.index);
try std.testing.expectEqual(@as(u8, 1), first_result.id.gen);
const second_result = try table.add(12, .{ .a = 1, .b = 2 });
try std.testing.expect(!second_result.added);
try std.testing.expectEqual(@as(?TestVal, null), (try table.release(first_result.id)));
val = (try table.release(first_result.id)) orelse unreachable;
try std.testing.expectEqual(@as(usize, 0), table.size());
try std.testing.expectEqual(@as(u32, 6), val.a);
try std.testing.expectEqual(@as(u32, 5), val.b);
}