Backlog/lib/p2/src/structures/sparse-set.zig

835 lines
29 KiB
Zig

const std = @import("std");
const ArrayList = std.ArrayList;
const ArrayListUnmanaged = std.ArrayListUnmanaged;
// can be index by an 18 bit value, and 262144 of anything ought to be enough... right?
pub const DefaultSparseSize = 262144 / 2;
pub fn SparseSet(comptime T: type) type {
return SparseSetAdvanced(T, DefaultSparseSize);
}
pub const IndexType = u24;
pub const GenerationType = u7;
pub const SetHandle = packed struct {
index: IndexType = 0,
generation: GenerationType = 0,
alive: bool = false,
pub fn hash(self: @This()) u32 {
return @as(u32, @bitCast(self));
}
pub fn eql(self: @This(), other: @This()) bool {
return self.hash() == other.hash();
}
};
pub fn SparseMultiSet(comptime T: type) type {
return SparseMultiSetAdvanced(T, DefaultSparseSize);
}
pub const ContainerListener = struct {
ptr: *anyopaque,
onHandleRemoved: *const fn (*anyopaque, u32, SetHandle) void,
onHandleAdded: *const fn (*anyopaque, u32, SetHandle) void,
};
// works by converting a datastructure into an AOS type.
pub fn SparseMultiSetAdvanced(comptime T: type, comptime SparseSize: u32) type {
return struct {
pub const SetType = std.MultiArrayList(T);
pub const InnerType = T;
allocator: std.mem.Allocator,
denseIndices: ArrayListUnmanaged(SetHandle),
dense: SetType,
sparse: []SetHandle,
containerID: u32 = 0,
containerListener: ?ContainerListener = null,
opCount: u32 = 0,
pub const Field = SetType.Field;
pub const Slice = SetType.Slice;
pub const IsMultiset = true;
pub const StableReferences = false;
pub fn init(allocator: std.mem.Allocator) @This() {
var self = @This(){
.allocator = allocator,
.denseIndices = .{},
.dense = .{},
.sparse = allocator.alloc(SetHandle, SparseSize) catch unreachable,
};
for (self.sparse, 0..) |_, i| {
self.sparse[i] = .{ .generation = 0, .index = 0x0, .alive = false };
}
return self;
}
pub fn create(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = init(allocator);
return self;
}
pub fn destroy(self: *@This()) void {
const allocator = self.allocator;
self.deinit();
allocator.destroy(self);
}
pub fn denseItems(self: *@This(), comptime field: Field) []FieldType(field) {
return self.dense.items(field);
}
pub fn deinit(self: *@This()) void {
self.dense.deinit(self.allocator);
self.denseIndices.deinit(self.allocator);
self.allocator.free(self.sparse);
}
pub fn readDense(self: @This(), offset: usize, comptime field: Field) *const FieldType(field) {
return &self.dense.items(field)[offset];
}
pub fn getDense(self: *@This(), offset: usize, comptime field: Field) *FieldType(field) {
return &self.dense.items(field)[offset];
}
// ----- sparse set features -----
pub fn handleFromSparseIndex(self: @This(), sparseIndex: IndexType) SetHandle {
var handle: SetHandle = self.sparse[@as(usize, @intCast(sparseIndex))];
handle.index = sparseIndex;
return handle;
}
pub fn sparseToDense(self: @This(), handle: SetHandle) ?usize {
const denseHandle = self.sparse[@as(usize, @intCast(handle.index))];
// todo: need to update generation
if (denseHandle.generation != handle.generation) // tombstone value
{
// Generation mismatch, this handle is totally dead.
return null;
}
if (denseHandle.alive == false) {
return null;
}
const denseIndex = @as(usize, @intCast(denseHandle.index));
if (denseIndex >= self.denseIndices.items.len) {
return null;
}
return denseIndex;
}
pub fn FieldType(comptime field: Field) type {
return std.meta.fieldInfo(T, field).type;
}
pub fn get(self: *@This(), handle: SetHandle, comptime field: Field) ?*FieldType(field) {
const denseIndex = self.sparseToDense(handle) orelse return null;
return &self.dense.items(field)[denseIndex];
}
// the idea behind a sparse array is that the sethandle is
// highly stable.
// if this container is registered as part of an ECS, this is unsafe to call directly
pub fn createObject(self: *@This(), initValue: T) !SetHandle {
var newSparseIndex = newRandomIndex();
var denseHandle = self.sparse[@as(usize, @intCast(newSparseIndex))];
while (denseHandle.alive == true) {
newSparseIndex = newRandomIndex();
denseHandle = self.sparse[@as(usize, @intCast(newSparseIndex))];
}
const generation = (denseHandle.generation + 1) % (std.math.maxInt(GenerationType));
return self.createObjectInternal(initValue, newSparseIndex, generation);
}
pub fn createObjectInternal(self: *@This(), initValue: T, newSparseIndex: IndexType, generation: GenerationType) !SetHandle {
const newDenseIndex = self.denseIndices.items.len;
self.sparse[@as(usize, @intCast(newSparseIndex))] = SetHandle{
.alive = true,
.generation = @as(GenerationType, @intCast(generation)),
.index = @as(IndexType, @intCast(newDenseIndex)),
};
const setHandle = SetHandle{
.alive = true,
.generation = generation,
.index = newSparseIndex,
};
try self.denseIndices.append(self.allocator, setHandle);
try self.dense.append(self.allocator, initValue);
std.debug.assert(self.sparse[newSparseIndex].index < self.dense.len);
self.opCount +%= 1;
if (self.containerListener) |l| {
l.onHandleAdded(l.ptr, @intCast(self.containerID), setHandle);
}
return setHandle;
}
pub fn denseToSparse(self: @This(), dense: u32) SetHandle {
return self.denseIndices.items[dense];
}
pub fn createWithHandle(self: *@This(), handle: SetHandle, initValue: T) !SetHandle {
const currentDenseHandle = self.sparse[handle.index];
if (currentDenseHandle.alive) {
return error.ObjectAlreadyExists;
}
return self.createObjectInternal(initValue, handle.index, handle.generation);
}
pub fn destroyObject(self: *@This(), handle: SetHandle) bool {
// to destroy an object
// get handle and get the dense position, swap and remove.
// Then insert the tombstone value into the sparse handle
// if this fails it means the object is already destroyed
const denseIndex = self.sparseToDense(handle) orelse return false;
// get the indec of the last object in the dense set
const tailDenseIndex = self.dense.len - 1;
// get the sparse index of the last object
const sparseIndexToSwap = self.denseIndices.items[tailDenseIndex];
// redirect the sparse index to the new position of the swapped object.
self.sparse[@as(usize, @intCast(sparseIndexToSwap.index))].index = @as(IndexType, @intCast(denseIndex));
// perform the swap and remove, mark the tombstone as well.
_ = self.dense.swapRemove(denseIndex);
_ = self.denseIndices.swapRemove(denseIndex);
self.sparse[@as(usize, @intCast(handle.index))].alive = false;
self.opCount +%= 1;
if (self.containerListener) |l| {
l.onHandleRemoved(l.ptr, self.containerID, handle);
}
return true;
}
var prng = std.Random.DefaultPrng.init(0x1234);
var rand = prng.random();
pub fn newRandomIndex() IndexType {
if (SparseSize == std.math.maxInt(IndexType)) {
return rand.int(IndexType);
}
return rand.int(IndexType) % @as(IndexType, @intCast(SparseSize));
}
// ecs interface
pub const EcsContainerInterfaceVTable = EcsContainerInterface.Implement(@This());
pub fn handleExists(self: @This(), handle: SetHandle) bool {
return self.sparseToDense(handle) != null;
}
pub fn getContainerID(self: @This()) u32 {
return self.containerID;
}
pub fn onRegister(self: *@This(), id: u32, listener: ContainerListener) void {
self.containerID = id;
self.containerListener = listener;
}
pub fn evictFromRegistry(self: *@This()) void {
self.containerListener = null;
}
pub fn getStateCount(self: @This()) u32 {
return self.opCount;
}
// replaces get.
// really nasty trick here. I'm relying on absolutely fucked aliasing
// for sparse-multiset
// which is not a type which can be represented in lua directly.
// rather the resulting component type should be a THIN component
// which only contains the handle.
//
// Zig MAY break this in the future. in which case I'll have to
// treat the sparse sets in this handle as C ABI with a fixed bit layout
pub fn getHandleRef(self: @This(), handle: SetHandle) *SetHandle {
if (self.handleExists(handle)) {
// std.debug.print("get: {p}\n", .{@as(*anyopaque, @ptrCast(&self.sparse[handle.index]))});
return &self.sparse[handle.index];
}
@panic("uh oh ");
}
pub fn createWithHandleECS(self: *@This(), handle: SetHandle) *SetHandle {
_ = self.createWithHandle(handle, .{}) catch @panic("unable to create with handle");
return self.getHandleRef(handle);
}
pub const ContainerTypeName = "SparseMultiSet";
};
}
// A quick little sparse set implementation, this feeds the core of the
// ECS. A sparse set provides Constant time random access to a range of objects through stable handles
// While providing dense memory locality for iterating.
//
// should probably never use this one outside of the base-set for checking entity existence.
pub fn SparseSetAdvanced(comptime T: type, comptime SparseSize: u32) type {
return struct {
allocator: std.mem.Allocator,
dense: ArrayListUnmanaged(struct {
value: T,
sparseIndex: SetHandle,
}),
sparse: []SetHandle,
containerID: u32 = 0,
containerListener: ?ContainerListener = null,
opCount: u32 = 0,
pub const StableReferences = false;
pub const InnerType = T;
pub fn getStateCount(self: @This()) u32 {
return self.opCount;
}
pub fn handleFromSparseIndex(self: @This(), sparseIndex: IndexType) SetHandle {
var handle: SetHandle = self.sparse[@as(usize, @intCast(sparseIndex))];
handle.index = sparseIndex;
return handle;
}
pub fn create(allocator: std.mem.Allocator) !*@This() {
const self = try allocator.create(@This());
self.* = init(allocator);
return self;
}
pub fn destroy(self: *@This()) void {
self.deinit();
self.allocator.destroy(self);
}
pub fn init(allocator: std.mem.Allocator) @This() {
var self = @This(){
.allocator = allocator,
.dense = .{},
.sparse = allocator.alloc(SetHandle, SparseSize) catch unreachable,
};
for (self.sparse, 0..) |_, i| {
self.sparse[i] = .{ .generation = 0, .index = 0x0, .alive = false };
}
return self;
}
pub fn readDense(self: @This(), offset: usize) *const T {
return &self.dense.items[offset].value;
}
pub fn getDense(self: *@This(), offset: usize) *T {
return &self.dense.items[offset].value;
}
pub fn count(self: @This()) usize {
return self.dense.items.len;
}
pub fn sparseToDense(self: @This(), handle: SetHandle) ?usize {
const denseHandle = self.sparse[@as(usize, @intCast(handle.index))];
// todo: need to update generation
if (denseHandle.generation != handle.generation) // tombstone value
{
// Generation mismatch, this handle is totally dead.
return null;
}
if (denseHandle.alive == false) {
return null;
}
const denseIndex = @as(usize, @intCast(denseHandle.index));
if (denseIndex >= self.dense.items.len) {
return null;
}
return denseIndex;
}
pub fn get(self: @This(), handle: SetHandle) ?*T {
const denseIndex = self.sparseToDense(handle) orelse return null;
return &self.dense.items[denseIndex].value;
}
pub fn destroyObject(self: *@This(), handle: SetHandle) void {
// to destroy an object
// get handle and get the dense position, swap and remove.
// Then insert the tombstone value into the sparse handle
if (@hasDecl(T, "deinitECS")) {
self.get(handle).?.deinitECS(handle);
}
const denseIndex = self.sparseToDense(handle) orelse return;
const tailDenseIndex = self.dense.items.len - 1;
const sparseIndexToSwap = self.dense.items[tailDenseIndex].sparseIndex;
self.opCount +%= 1;
self.sparse[@as(usize, @intCast(sparseIndexToSwap.index))].index = @as(IndexType, @intCast(denseIndex));
_ = self.dense.swapRemove(denseIndex);
self.sparse[@as(usize, @intCast(handle.index))].alive = false;
if (self.containerListener) |l| {
l.onHandleRemoved(l.ptr, self.containerID, handle);
}
}
var prng = std.Random.DefaultPrng.init(0x1234);
var rand = prng.random();
fn newRandomIndex() IndexType {
if (SparseSize == std.math.maxInt(IndexType)) {
return rand.int(IndexType);
}
return rand.int(IndexType) % @as(IndexType, @intCast(SparseSize));
}
// the idea behind a sparse array is that the sethandle is
// highly stable.
pub fn createObject(self: *@This(), initValue: T) !SetHandle {
var newSparseIndex = newRandomIndex();
self.opCount +%= 1;
var denseHandle = self.sparse[@as(usize, @intCast(newSparseIndex))];
while (denseHandle.alive == true) {
newSparseIndex = newRandomIndex();
denseHandle = self.sparse[@as(usize, @intCast(newSparseIndex))];
}
const generation = (denseHandle.generation + 1) % (std.math.maxInt(GenerationType));
const newDenseIndex = self.dense.items.len;
const sparseToDenseHandle = SetHandle{
.alive = true,
.generation = @as(GenerationType, @intCast(generation)),
.index = @as(IndexType, @intCast(newDenseIndex)),
};
const denseToSparseHandle = SetHandle{
.alive = true,
.generation = @as(GenerationType, @intCast(generation)),
.index = @as(IndexType, @intCast(newSparseIndex)),
};
try self.dense.append(self.allocator, .{
.value = initValue,
.sparseIndex = denseToSparseHandle,
});
self.sparse[@as(usize, @intCast(newSparseIndex))] = sparseToDenseHandle;
const setHandle = SetHandle{
.alive = true,
.generation = generation,
.index = newSparseIndex,
};
return setHandle;
}
pub const ConstructResult = struct {
ptr: *T,
handle: SetHandle,
};
// Will fail if the handle already exists.
pub fn createWithHandle(self: *@This(), handle: SetHandle, initValue: T) !ConstructResult {
if (handle.index >= DefaultSparseSize) {
std.debug.print("this should never happen handle index is huge: {x} {d}\n", .{ handle.index, handle.index });
@panic("impossible handle");
}
var currentDenseHandle = self.sparse[handle.index];
if (currentDenseHandle.alive) {
return error.ObjectAlreadyExists;
}
currentDenseHandle.generation = handle.generation;
currentDenseHandle.alive = true;
return try self.createAndGetInternal(currentDenseHandle, handle.index, initValue, false);
}
pub fn createWithHandleECS(self: *@This(), handle: SetHandle) *T {
const rv = (self.createWithHandle(handle, .{}) catch @panic("unable to create")).ptr;
if (@hasDecl(T, "initECS")) {
rv.initECS(handle);
}
return rv;
}
fn createAndGetInternal(self: *@This(), denseHandle: SetHandle, sparseIndex: IndexType, initValue: T, comptime bumpGeneration: bool) !ConstructResult {
const newDenseIndex = self.dense.items.len;
try self.dense.append(self.allocator, .{
.value = initValue,
.sparseIndex = .{
.index = sparseIndex,
.generation = denseHandle.generation,
.alive = true,
},
});
var generation = denseHandle.generation;
if (bumpGeneration) {
generation = (generation + 1) % (std.math.maxInt(GenerationType));
}
self.sparse[@as(usize, @intCast(sparseIndex))] = SetHandle{
.alive = true,
.generation = @as(GenerationType, @intCast(generation)),
.index = @as(IndexType, @intCast(newDenseIndex)),
};
const setHandle = SetHandle{
.alive = true,
.generation = generation,
.index = sparseIndex,
};
const rv = ConstructResult{
.ptr = &self.dense.items[@as(usize, @intCast(newDenseIndex))].value,
.handle = setHandle,
};
if (self.containerListener) |l| {
l.onHandleAdded(l.ptr, self.containerID, setHandle);
}
return rv;
}
pub fn createAndGet(self: *@This(), initValue: T) !ConstructResult {
var newSparseIndex = newRandomIndex();
var denseHandle = self.sparse[@as(usize, @intCast(newSparseIndex))];
while (denseHandle.alive == true) {
newSparseIndex = newRandomIndex();
denseHandle = self.sparse[@as(usize, @intCast(newSparseIndex))];
}
return try self.createAndGetInternal(denseHandle, newSparseIndex, initValue, true);
}
pub fn deinit(self: *@This()) void {
self.allocator.free(self.sparse);
self.dense.deinit(self.allocator);
}
pub const EcsContainerInterfaceVTable = EcsContainerInterface.Implement(@This());
pub fn handleExists(self: @This(), handle: SetHandle) bool {
return self.sparseToDense(handle) != null;
}
pub fn getContainerID(self: @This()) u32 {
return self.containerID;
}
pub fn onRegister(self: *@This(), id: u32, listener: ContainerListener) void {
self.containerID = id;
self.containerListener = listener;
}
// might never need to call this one...
pub fn evictFromRegistry(self: *@This()) void {
self.containerListener = null;
}
pub const ContainerTypeName = "SparseSet";
};
}
// slowish look-up, fast-ish iteration time,
// very little memory overhead, stable pointers
// good all-around choice if you have a small
// number of these objects around, and the object itself is quite big.
pub fn SparseMap(comptime T: type) type {
return struct {
backingAllocator: std.mem.Allocator,
arena: std.heap.ArenaAllocator,
map: std.AutoHashMapUnmanaged(SetHandle, *T) = .{},
listEntriesByHandle: std.AutoHashMapUnmanaged(SetHandle, u32) = .{},
list: std.ArrayListUnmanaged(*T) = .{},
handles: std.ArrayListUnmanaged(SetHandle) = .{},
containerID: u32 = 0,
containerListener: ?ContainerListener = null,
opCount: u32 = 0,
pub const InnerType = T;
pub const StableReferences = true;
pub fn create(backingAllocator: std.mem.Allocator) !*@This() {
const self = try backingAllocator.create(@This());
self.* = .{
.arena = std.heap.ArenaAllocator.init(backingAllocator),
.backingAllocator = backingAllocator,
};
return self;
}
pub fn indexToHandle(self: @This(), index: usize) SetHandle {
return self.handles.items[index];
}
pub fn destroyObject(self: *@This(), handle: SetHandle) void {
std.debug.assert(self.map.contains(handle));
if (@hasDecl(T, "deinitECS")) {
self.map.get(handle).?.deinitECS(handle);
}
const alloc = self.allocator();
const index = self.listEntriesByHandle.get(handle).?;
alloc.destroy(self.map.get(handle).?);
_ = self.map.remove(handle);
_ = self.listEntriesByHandle.remove(handle);
_ = self.list.swapRemove(index); // oops.
const swappedHandle = self.handles.getLastOrNull();
_ = self.handles.swapRemove(index);
if (swappedHandle) |swapped|
self.listEntriesByHandle.put(alloc, swapped, index) catch unreachable;
self.opCount +%= 1;
if (self.containerListener) |l| {
l.onHandleRemoved(l.ptr, self.containerID, handle);
}
}
pub fn createWithHandle(self: *@This(), handle: SetHandle, initValue: T) !*T {
std.debug.assert(!self.map.contains(handle));
const alloc = self.allocator();
const new = try alloc.create(T);
new.* = initValue;
try self.map.put(alloc, handle, new);
try self.list.append(alloc, new);
try self.handles.append(alloc, handle);
try self.listEntriesByHandle.put(alloc, handle, @intCast(self.list.items.len - 1));
if (self.containerListener) |l| {
l.onHandleAdded(l.ptr, self.containerID, handle);
}
self.opCount +%= 1;
return new;
}
pub fn createWithHandleECS(self: *@This(), handle: SetHandle) *T {
const rv = self.createWithHandle(handle, .{}) catch @panic("Unable to create");
if (@hasDecl(T, "initECS")) {
rv.initECS(handle);
}
return rv;
}
pub fn allocator(self: *@This()) std.mem.Allocator {
return self.arena.allocator();
}
pub fn destroy(self: *@This()) void {
self.arena.deinit();
self.backingAllocator.destroy(self);
}
// == interface below ==
pub const EcsContainerInterfaceVTable = EcsContainerInterface.Implement(@This());
pub fn get(self: @This(), handle: SetHandle) ?*anyopaque {
return self.map.get(handle);
}
pub fn handleExists(self: @This(), handle: SetHandle) bool {
return self.map.contains(handle);
}
pub fn getContainerID(self: @This()) u32 {
return self.containerID;
}
pub fn onRegister(self: *@This(), id: u32, listener: ContainerListener) void {
self.containerID = id;
self.containerListener = listener;
}
// might never need to call this one...
pub fn evictFromRegistry(self: *@This()) void {
self.containerListener = null;
}
pub fn getStateCount(self: @This()) u32 {
return self.opCount;
}
pub const ContainerTypeName = "SparseMap";
};
}
// this is quickly becoming the ecs containers file
// todo.. move EcsContainerInterface into somewhere else.
const interface = @import("interface.zig");
pub const EcsContainerInterface = interface.MakeInterface("EcsContainerInterfaceVTable", struct {
containerTypeName: []const u8,
componentName: []const u8,
handleExists: *const fn (*const anyopaque, SetHandle) bool,
get: *const fn (*const anyopaque, SetHandle) ?*anyopaque,
createWithHandle: *const fn (*anyopaque, SetHandle) *anyopaque,
destroyObject: *const fn (*anyopaque, SetHandle) void,
getContainerID: *const fn (*const anyopaque) u32,
onRegister: *const fn (*anyopaque, u32, ContainerListener) void,
evictFromRegistry: *const fn (*anyopaque) void,
getStateCount: *const fn (*anyopaque) u32, // returns an internal number representing the state of the container
pub const Reference = struct {
vtable: *const @This(),
ptr: *anyopaque,
};
pub fn Implement(comptime TargetType: type) @This() {
const Wrap = struct {
pub fn handleExists(p: *const anyopaque, handle: SetHandle) bool {
var ptr = @as(*const TargetType, @ptrCast(@alignCast(p)));
return ptr.handleExists(handle);
}
// gets a function. assuming it exists
pub fn get(p: *const anyopaque, handle: SetHandle) ?*anyopaque {
// std.debug.print("get {p}\n", .{p});
var ptr = @as(*const TargetType, @ptrCast(@alignCast(p)));
if (@hasDecl(TargetType, "getHandleRef")) {
return ptr.getHandleRef(handle);
}
return @ptrCast(ptr.get(handle));
}
pub fn destroyObject(p: *anyopaque, handle: SetHandle) void {
// std.debug.print("creat with handle {p}\n", .{p});
var ptr = @as(*TargetType, @ptrCast(@alignCast(p)));
ptr.destroyObject(handle);
}
pub fn createWithHandle(p: *anyopaque, handle: SetHandle) *anyopaque {
// std.debug.print("creat with handle {p}\n", .{p});
var ptr = @as(*TargetType, @ptrCast(@alignCast(p)));
return ptr.createWithHandleECS(handle);
}
pub fn getStateCount(p: *const anyopaque) u32 {
// std.debug.print("getstatecount {p}\n", .{p});
var ptr = @as(*const TargetType, @ptrCast(@alignCast(p)));
return ptr.getStateCount();
}
pub fn getContainerID(p: *const anyopaque) u32 {
var ptr = @as(*const TargetType, @ptrCast(@alignCast(p)));
return ptr.getContainerID();
}
pub fn onRegister(p: *anyopaque, id: u32, listener: ContainerListener) void {
var ptr = @as(*TargetType, @ptrCast(@alignCast(p)));
return ptr.onRegister(id, listener);
}
pub fn evictFromRegistry(p: *anyopaque) void {
var ptr = @as(*TargetType, @ptrCast(@alignCast(p)));
return ptr.evictFromRegistry();
}
};
return .{
.containerTypeName = TargetType.ContainerTypeName,
.componentName = @typeName(TargetType.InnerType),
.handleExists = Wrap.handleExists,
.get = Wrap.get,
.createWithHandle = Wrap.createWithHandle,
.destroyObject = Wrap.destroyObject,
.getContainerID = Wrap.getContainerID,
.onRegister = Wrap.onRegister,
.evictFromRegistry = Wrap.evictFromRegistry,
.getStateCount = Wrap.getStateCount,
};
}
});
test "sparse-multiset-recycle-handles" {
const testHandle: SetHandle = .{
.index = 420,
.alive = true,
.generation = 12,
};
const testHandle2: SetHandle = .{
.index = 421,
.alive = true,
.generation = 12,
};
const TestStruct = struct {
wutang: u32 = 0,
lmao: u32 = 0,
};
var testSet = SparseMultiSet(TestStruct).init(std.testing.allocator);
defer testSet.deinit();
_ = testSet.createWithHandleECS(testHandle);
_ = testSet.createWithHandleECS(testHandle2);
std.debug.assert(testSet.get(testHandle, .wutang) != null);
std.debug.assert(testSet.get(testHandle2, .wutang) != null);
}